diff --git a/.github/workflows/job_build_tr1.yml b/.github/workflows/job_build_tr1.yml index 47dd2e6ef..15780ce8e 100644 --- a/.github/workflows/job_build_tr1.yml +++ b/.github/workflows/job_build_tr1.yml @@ -3,6 +3,10 @@ name: Build TR1X and the installer on: workflow_call: inputs: + platform: + type: string + description: "Platform to build for" + required: true target: type: string description: "Target to build for" @@ -12,15 +16,6 @@ jobs: build: name: Build release assets runs-on: ubuntu-latest - strategy: - matrix: - include: - - platform: linux - just_target: tr1-package-linux ${{ inputs.target }} - - platform: win - just_target: tr1-package-win-all ${{ inputs.target }} - - platform: win-installer - just_target: tr1-package-win-installer ${{ inputs.target }} steps: - name: Install dependencies @@ -37,13 +32,17 @@ jobs: name: Prepare variables run: echo "version=$(just output-current-version 1)" >> $GITHUB_OUTPUT - - name: Package asset (${{ matrix.platform }}) - run: just ${{ matrix.just_target }} + - name: Download large assets + if: ${{ inputs.target == 'release' }} + run: just download-assets 1 + + - name: Package asset (${{ inputs.platform }}) + run: just tr1-package-${{ inputs.platform }} ${{ inputs.target }} - name: Upload the artifact uses: actions/upload-artifact@v4 with: - name: TR1X-${{ steps.vars.outputs.version }}-${{ matrix.platform }} + name: TR1X-${{ steps.vars.outputs.version }}-${{ inputs.platform }} path: | *.zip *.exe diff --git a/.github/workflows/job_build_tr2.yml b/.github/workflows/job_build_tr2.yml index 71679cd01..0dc810c1c 100644 --- a/.github/workflows/job_build_tr2.yml +++ b/.github/workflows/job_build_tr2.yml @@ -3,6 +3,10 @@ name: Build TR2X and the installer on: workflow_call: inputs: + platform: + type: string + description: "Platform to build for" + required: true target: type: string description: "Target to build for" @@ -12,15 +16,6 @@ jobs: build: name: Build release assets runs-on: ubuntu-latest - strategy: - matrix: - include: - - platform: linux - just_target: tr2-package-linux ${{ inputs.target }} - - platform: win - just_target: tr2-package-win-all ${{ inputs.target }} - - platform: win-installer - just_target: tr2-package-win-installer ${{ inputs.target }} steps: - name: Install dependencies @@ -37,13 +32,17 @@ jobs: name: Prepare variables run: echo "version=$(just output-current-version 2)" >> $GITHUB_OUTPUT - - name: Package asset (${{ matrix.platform }}) - run: just ${{ matrix.just_target }} + - name: Download large assets + if: ${{ inputs.target == 'release' }} + run: just download-assets 2 + + - name: Package asset (${{ inputs.platform }}) + run: just tr2-package-${{ inputs.platform }} ${{ inputs.target }} - name: Upload the artifact uses: actions/upload-artifact@v4 with: - name: TR2X-${{ steps.vars.outputs.version }}-${{ matrix.platform }} + name: TR2X-${{ steps.vars.outputs.version }}-${{ inputs.platform }} path: | *.zip *.exe diff --git a/.github/workflows/pr_builds.yml b/.github/workflows/pr_builds.yml index ba549eff8..16112e312 100644 --- a/.github/workflows/pr_builds.yml +++ b/.github/workflows/pr_builds.yml @@ -11,34 +11,52 @@ on: - '!develop' jobs: - package_tr1_multiplatform: - name: Build TR1 + package_tr1_linux: + name: TR1 (Linux) uses: ./.github/workflows/job_build_tr1.yml with: - target: 'debug' + platform: linux + target: debug + secrets: inherit + + package_tr1_win: + name: TR1 (Windows) + uses: ./.github/workflows/job_build_tr1.yml + with: + platform: win-all + target: debug secrets: inherit package_tr1_mac: - name: Build TR1 + name: TR1 (Mac) if: vars.MACOS_ENABLE == 'true' uses: ./.github/workflows/job_build_tr1_macos.yml with: - target: 'debug' + target: debug let_mac_fail: true secrets: inherit - package_tr2_multiplatform: - name: Build TR2 + package_tr2_linux: + name: TR2 (Linux) uses: ./.github/workflows/job_build_tr2.yml with: - target: 'debug' + platform: linux + target: debug + secrets: inherit + + package_tr2_win: + name: TR2 (Windows) + uses: ./.github/workflows/job_build_tr2.yml + with: + platform: win-all + target: debug secrets: inherit package_tr2_mac: - name: Build TR2 + name: TR2 (Mac) if: vars.MACOS_ENABLE == 'true' uses: ./.github/workflows/job_build_tr2_macos.yml with: - target: 'debug' + target: debug let_mac_fail: true secrets: inherit diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b6dbe4548..596638267 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -9,16 +9,24 @@ on: - develop jobs: - package_tr1_multiplatform: - name: Build TR1 - if: vars.PRERELEASE_ENABLE == 'true' + package_tr1_linux: + name: TR1 (Linux) uses: ./.github/workflows/job_build_tr1.yml with: - target: 'debug' + platform: linux + target: debug + secrets: inherit + + package_tr1_win: + name: TR1 (Windows) + uses: ./.github/workflows/job_build_tr1.yml + with: + platform: win-all + target: debug secrets: inherit package_tr1_mac: - name: Build TR1 + name: TR1 (Mac) if: | vars.PRERELEASE_ENABLE == 'true' && vars.MACOS_ENABLE == 'true' @@ -28,16 +36,24 @@ jobs: let_mac_fail: true secrets: inherit - package_tr2_multiplatform: - name: Build TR2 - if: vars.PRERELEASE_ENABLE == 'true' + package_tr2_linux: + name: TR2 (Linux) uses: ./.github/workflows/job_build_tr2.yml with: - target: 'debug' + platform: linux + target: debug + secrets: inherit + + package_tr2_win: + name: TR2 (Windows) + uses: ./.github/workflows/job_build_tr2.yml + with: + platform: win-all + target: debug secrets: inherit package_tr2_mac: - name: Build TR2 + name: TR2 (Mac) if: | vars.PRERELEASE_ENABLE == 'true' && vars.MACOS_ENABLE == 'true' @@ -48,12 +64,14 @@ jobs: secrets: inherit publish_prerelease: - if: vars.PRERELEASE_ENABLE == 'true' + if: always() && (vars.PRERELEASE_ENABLE == 'true') name: Create a prerelease needs: - - package_tr1_multiplatform + - package_tr1_linux + - package_tr1_win - package_tr1_mac - - package_tr2_multiplatform + - package_tr2_linux + - package_tr2_win - package_tr2_mac with: draft: false diff --git a/.github/workflows/release_tr1.yml b/.github/workflows/release_tr1.yml index 0c3d24dfa..9881c9baa 100644 --- a/.github/workflows/release_tr1.yml +++ b/.github/workflows/release_tr1.yml @@ -28,30 +28,51 @@ on: default: github.ref_name jobs: - package_multiplatform: - name: Build release assets + package_tr1_linux: + name: Build TR1 (Linux) if: vars.RELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build_tr1.yml with: - target: "release" + platform: linux + target: release + secrets: inherit + + package_tr1_win: + name: Build TR1 (Windows) + if: vars.RELEASE_ENABLE == 'true' + uses: ./.github/workflows/job_build_tr1.yml + with: + platform: win-all + target: release + secrets: inherit + + package_tr1_win_installer: + name: Build TR1 (Windows installer) + if: vars.RELEASE_ENABLE == 'true' + uses: ./.github/workflows/job_build_tr1.yml + with: + platform: win-installer + target: release secrets: inherit package_mac: - name: "Build release assets (mac)" + name: Build TR1 (Mac) if: | vars.RELEASE_ENABLE == 'true' && vars.MACOS_ENABLE == 'true' uses: ./.github/workflows/job_build_tr1_macos.yml with: - target: "release" + target: release let_mac_fail: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }} secrets: inherit publish_release: - if: vars.RELEASE_ENABLE == 'true' + if: always() && (vars.RELEASE_ENABLE == 'true') name: Create a GitHub release needs: - - package_multiplatform + - package_tr1_linux + - package_tr1_win + - package_tr1_win_installer - package_mac with: draft: ${{ inputs.draft || false }} diff --git a/.github/workflows/release_tr2.yml b/.github/workflows/release_tr2.yml index b82264f43..f4b3e0359 100644 --- a/.github/workflows/release_tr2.yml +++ b/.github/workflows/release_tr2.yml @@ -28,30 +28,52 @@ on: default: github.ref_name jobs: - package_multiplatform: - name: Build release assets + package_tr2_linux: + name: Build TR2 (Linux) if: vars.RELEASE_ENABLE == 'true' uses: ./.github/workflows/job_build_tr2.yml with: - target: "release" + platform: linux + target: release + secrets: inherit + + package_tr2_win: + name: Build TR2 (Windows) + if: vars.RELEASE_ENABLE == 'true' + uses: ./.github/workflows/job_build_tr2.yml + with: + platform: win-all + target: release + secrets: inherit + + package_tr2_win_installer: + name: Build TR2 (Windows installer) + if: vars.RELEASE_ENABLE == 'true' + uses: ./.github/workflows/job_build_tr2.yml + with: + platform: win-installer + target: release secrets: inherit package_mac: - name: "Build release assets (mac)" + name: Build TR2 (Mac) if: | vars.RELEASE_ENABLE == 'true' && vars.MACOS_ENABLE == 'true' uses: ./.github/workflows/job_build_tr2_macos.yml with: - target: "release" + target: release let_mac_fail: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }} secrets: inherit publish_release: - if: vars.RELEASE_ENABLE == 'true' + if: always() && (vars.RELEASE_ENABLE == 'true') name: Create a GitHub release needs: - - package_multiplatform + - package_tr2_linux + - package_tr2_win + - package_tr2_win_installer + - package_mac with: draft: ${{ inputs.draft || false }} prerelease: ${{ inputs.draft || false }} diff --git a/.gitignore b/.gitignore index ff2ce821f..0e5438a15 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ Release/ **/subprojects/dwarfstack-*/ src/tr1/subprojects/dwarfstack.wrap src/tr2/subprojects/dwarfstack.wrap + +data/tr1/ship/data/images/ +data/tr2/ship/data/images/ +data/tr2/ship/music/ diff --git a/README.md b/README.md index 513b81016..a1ea02926 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,9 @@ Please refer to the [detailed documentation](docs/tr2/). further refined our identity by adopting the name TR1X. Meanwhile, TR2Main follows a completely separate and unique path, unconnected to our development work. + +## Credits + +- Endless GitHub contributors. +- TR1 title screen image by Kidd Bowyer. HD assets by goblan and posix. +- TR2 HD images by Arsunt. diff --git a/data/tr1/icon.psd b/data/tr1/icon.psd deleted file mode 100644 index 43c951f5b..000000000 Binary files a/data/tr1/icon.psd and /dev/null differ diff --git a/data/tr1/images/atlantis.png b/data/tr1/images/atlantis.png deleted file mode 100644 index 2f53f6967..000000000 Binary files a/data/tr1/images/atlantis.png and /dev/null differ diff --git a/data/tr1/images/credits_1.png b/data/tr1/images/credits_1.png deleted file mode 100644 index 696826fa2..000000000 Binary files a/data/tr1/images/credits_1.png and /dev/null differ diff --git a/data/tr1/images/credits_2.png b/data/tr1/images/credits_2.png deleted file mode 100644 index 5acd9d811..000000000 Binary files a/data/tr1/images/credits_2.png and /dev/null differ diff --git a/data/tr1/images/credits_3.png b/data/tr1/images/credits_3.png deleted file mode 100644 index 4f14f177c..000000000 Binary files a/data/tr1/images/credits_3.png and /dev/null differ diff --git a/data/tr1/images/credits_3_alt.png b/data/tr1/images/credits_3_alt.png deleted file mode 100644 index fe37e730d..000000000 Binary files a/data/tr1/images/credits_3_alt.png and /dev/null differ diff --git a/data/tr1/images/credits_ps1.png b/data/tr1/images/credits_ps1.png deleted file mode 100644 index 3f8e660a1..000000000 Binary files a/data/tr1/images/credits_ps1.png and /dev/null differ diff --git a/data/tr1/images/credits_ub.png b/data/tr1/images/credits_ub.png deleted file mode 100644 index 191137c8d..000000000 Binary files a/data/tr1/images/credits_ub.png and /dev/null differ diff --git a/data/tr1/images/egypt.png b/data/tr1/images/egypt.png deleted file mode 100644 index caee294e2..000000000 Binary files a/data/tr1/images/egypt.png and /dev/null differ diff --git a/data/tr1/images/eidos.png b/data/tr1/images/eidos.png deleted file mode 100644 index a48e3375e..000000000 Binary files a/data/tr1/images/eidos.png and /dev/null differ diff --git a/data/tr1/images/end.png b/data/tr1/images/end.png deleted file mode 100644 index 1e3870d86..000000000 Binary files a/data/tr1/images/end.png and /dev/null differ diff --git a/data/tr1/images/greece.png b/data/tr1/images/greece.png deleted file mode 100644 index 8734ede4f..000000000 Binary files a/data/tr1/images/greece.png and /dev/null differ diff --git a/data/tr1/images/greece_saturn.png b/data/tr1/images/greece_saturn.png deleted file mode 100644 index 78bb31de5..000000000 Binary files a/data/tr1/images/greece_saturn.png and /dev/null differ diff --git a/data/tr1/images/gym.png b/data/tr1/images/gym.png deleted file mode 100644 index 3ad6f4752..000000000 Binary files a/data/tr1/images/gym.png and /dev/null differ diff --git a/data/tr1/images/install.png b/data/tr1/images/install.png deleted file mode 100644 index 98cce3b46..000000000 Binary files a/data/tr1/images/install.png and /dev/null differ diff --git a/data/tr1/images/peru.png b/data/tr1/images/peru.png deleted file mode 100644 index 4142b951c..000000000 Binary files a/data/tr1/images/peru.png and /dev/null differ diff --git a/data/tr1/images/title.png b/data/tr1/images/title.png deleted file mode 100644 index e11218272..000000000 Binary files a/data/tr1/images/title.png and /dev/null differ diff --git a/data/tr1/images/title_og_alt.png b/data/tr1/images/title_og_alt.png deleted file mode 100644 index eee8de9fd..000000000 Binary files a/data/tr1/images/title_og_alt.png and /dev/null differ diff --git a/data/tr1/images/title_ub.png b/data/tr1/images/title_ub.png deleted file mode 100644 index 55ece1bd9..000000000 Binary files a/data/tr1/images/title_ub.png and /dev/null differ diff --git a/data/tr1/installer icon.ai b/data/tr1/installer icon.ai deleted file mode 100755 index 6b2b1d3bf..000000000 --- a/data/tr1/installer icon.ai +++ /dev/null @@ -1,727 +0,0 @@ -%PDF-1.5 % -1 0 obj <> endobj 2 0 obj <>stream - - - - - Adobe Illustrator CC 23.0 (Windows) - 2022-07-14T11:32:11+02:00 - 2022-07-14T11:43:29+02:00 - 2022-07-14T11:43:29+02:00 - - - - 248 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAD4AwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYqpXd3aWdtLdXk0dtawqXmnmYJGijqzMxAAHicVfP/5lf85YaTYGXTfI8S6leUKn VrgMtqh6fukPFpSPFuK/6wyBkzEXzpP+ZPn2XzKPMsmuXY1tT8F2JCpVTv6aoKIIz/Jx4+2QtlT6 A/LP/nLSzuBFpvnyEWs+yrrVshMTHxnhWrJ/rJUf5KjJiTExfRGn6jp+pWUV9p9zFeWc68obmB1k jdfFWUkHJsERirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiryr 8zP+cifJfk1ZrSzYa3rcZ4GztnHpROQaCeb4gvT7Khm8QMiZMhF8o/mD+bXnbz3dF9avSLFW5QaX b1jtY/D4Knmw/mck++QJtmBSG8ifll5z88Xv1fQLBpYUYLcX8tY7WGv+/JSKV78Vq3gMQLUmn0JD /wA4eaD/AIY+rT63OfMVea3yIv1VSR/d+gfjZf8AK5g1326ZLgY8TwP8wPym87eRLrhrdkTZM3GD U7eslrJ4Ueg4k/yuAfbIkUyBtQ8ifmf5z8j3nr6BftFA7criwlrJay/68RI37clo3viDSkW+qvyz /wCcl/Jvmz0rDWCug62/wiKdwbaVun7qc0AJ/lengC2TEmBi9hyTF2KuxV2KuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV83/85aeevNej3GlaFpWoSWOnajbSy3qwHg8pD8ApkHx8 adVBoe9chIs4h826DoOta9cDTdGspr++mkThbwIXalGBY0+yorux2HfIM30h+Wf/ADiXawelqXny YXEooy6LbORED4TzLQv/AKqUH+UcmIsDJ9E6bpmnaZZRWGnW0VnZQLxhtoEWONB4KqgAZNgicVUr uztL21ltLyCO5tZlKTQTKskbqeqsjAgj54q+ffzM/wCcTtKv/V1LyPMunXZqzaTOSbZz1PpSbtEf Y1X/AFRkDFmJPmXzH5Y8weWtSfTNdsJtPvY9zFMtOQ6ckYVV1/ylJGQpm9+/5xK89ear/wAx3vla +1CS70W202S6tbeY8zC8c8MYEbn4gnGU/DWnhk4lhIPqPJsHYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq+Uf+cyv+Um8u/wDMFN/ydyubODv+cNf+Um8xf8wUX/J3GCzfVxIA qdgO+WMENNeou0fxHx7Zh5dYBtHcsxBCG4kaSvP4xv8AIH/azAOaRN3u2UETDfDpLt/lDMzFrOkm Bh3IpWVhVSCPEZnAgiw1vmb/AJzS/wCmO/7eX/YrkZs4MY/5w9/8mZqf/bFn/wCoq1wQ5plyfYOW NbsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVcSACSaAbknFXmvnL/nIb8r/ACuX hk1L9KXybGz00C4YHwaSqwrTuC9fbImQSIl8sfnT+bQ/MjW7O9j039G29hC0EKNL6ruGflyYhUA+ Qr88gTbYBTN/+cQ9Qhs/NeuGVuIksUAB2FRMvU9tsqyZ44xZ6p4DLk+ppJpJTVjUdQB0+j781+XP KfPkojSzKWSxVPqs/ZlUU+RP9cVX4qujkeNuSGh7+B+eWY8soHZBFvnD/nMW/juT5RRSDJF+kPU4 7j4vqtP1ZsMWoGT3hHAQ8n/J38zT+XXmqXW/0f8ApKO4tXspoPU9FgkkschdW4vuPR6EZeDSCLfV fk3/AJyQ/K/zKUge/OjXz7C21ICFSf8AJnBaH5VYE+GTEgwMS9QjkjkjWSNg8bgMjqagg7ggjqDk mLeKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvJvzM/5yP8k+TjLYWLDXddjJVrO2cCGJ h2mnoygjuq8mrsaZEyZCL5g89/nR+YXnqZoL+9aDT5DSPSLLlFAQdqMoJeX/AGZPtkDJmIqvlj8k POmshJrqJdJs2ofUuq+qQf5YR8Vf9bjmBl7Qxw5eouRDTyPkhPzP/L+z8m3WnWdtdSXklzC0k0sg VAWD8QEQVI+knJaTUnKCSKRmx8FMx/5xip+n9a8fqkf/ACcyjtP6R72Wn5l9JWepT25AU84v5D0+ jNVDIYt88Ykndrf21xsjUf8AkbY/RmXCYlycWUDFE5Ngh7m+t7YfvG+Lsg3ORlMR5sowMuST3eq3 E9VT93H4DqfmcxZ5iXKhiAfPP/OUn/TM/wDR9/2L5sOy/wCL4fpatT0eb/lf5JsvOGuXemXVxJai Kze5iliCn41ljQcgw3Wkh6EfPMzV6g4ogjfdrw4+I0m/mb8ifOWkh5rAJrFqtTW3qswA8YW3PyQt lWLtHHLn6Synp5Dlul3kn82fzC8hXXo6ZfSLbRtSbSLwNJb1ruDExBjPiUKnM+Mu5xzF9N/lp/zk 15O81NFp+tAaBrTkIiTPW1mY7D05iF4kn9l6eALZYJMDF7ICCKjpkmLsVdirsVdirsVdirsVdirs VdirsVdirsVS7zB5i0Ty7pM+r61eR2On2wrLPKdvZVAqzM3QKoJPbFXyN+bf/OS3mHzS02leWjJo 3l81RpFPG7uV6fvGU/u0P8in5k9MrMmwRYf5E/KHzH5pCXcg/R2kNv8AXJVJaQf8Ux7Fv9bYe+YG o1sMe3OTkY8Bl7nvHlryH5N8mWbXFvFHHJGtbjVLtlMlOhJkaiovstBmmy6jJlNH5BzYY4wYv5q/ P7y1ppe30WJtWulqPVqY7cH/AFyOT/QKHxzIw9mzlvL0hqnqQOW7xTzl531vzdfx3mqGNTCpSCKF OKIpNSNyzH6Tm3waeOIVFxJ5DI7p1+UXn+x8ma9PeX9tJcWd3EIJWiI5x/EHDhWoH+z0qMr1enOW NDmGWKfCX1JoHmXQvMNgt/o15HeWx2LIfiU9eLoaMjezDNDkxygakKcyMgeSZgkGoND45C2SMGrX gh9Plv8Az/tUpl3jypq8GNoNmLEkmpPUnKibbAEBrWu6Podg9/q13HZWidZZWpU0rxUdWY02VQTk oY5SNAWVJA5vmP8AOb8y9N8639hFptvJHZaX6wjuJSA0xm9Op4D7IHpbVNTXtm80emOIG+ZcLLkE uTE/KXm/WPKuqnUtKaMTvGYZFlTmjRsysVIqD9pB0IOX5sEckakxhMxNh7X5W/5yD0G+KW+vW7aZ cHb6xHWW3J8TQc0+4/PNTm7NkN4m3LhqQeezMte8o+TPO2npPcxxXayL/o+pWrL6gH+RKtaj2NR7 ZiY8+TEaG3k2yhGYeF+e/wAmPMHltZL2yrqmkLu00a0liH/FsYrsP5l28aZudPro5NjtJw8mAx3G 4Tv8pf8AnIvzN5MaHTNWL6z5bWii3dq3FuvT9xI37I/3223hxzYCVOMYvsDyp5t8v+a9Gh1jQrxL yxl25LsyOOsciH4kcV3B/VlgLWQm+KuxV2KuxV2KuxV2KuxV2KuxV2Ksd8+efvLnkjQZNZ1yf04g eFvbpQzTy0qI4lqKn8B1OAmkgW+I/wAxvzM82fmVr6S3gf6uHKaXo9vyZIgxoAqjd5G/aalT7Cgy qUupbIxej/lt+R1tYCLVfNMa3F9s0OmmjRRd6y9pG/yfsj37aTVdoE+mHLvc7Fp63kyfz5+a/l7y kjWiUvdXCjhYREAJUbGV9wg9uvtTfMfT6OWXflHvbMmYR97wHzH5x83+db8JdyvMpNYNPgBWFPcI PCv2mqffN1iwY8I2+bhmUshpMdG/Lh2pLqsvEdfq8Rqf9k/T7vvyrJq/5rlY9F/OZHP5P8uy2f1U WixqPsyptID48zUn6a5jjUTBu3JOmgRVMJ1zyPqenK8tsDd2ta8kHxqBX7S/xGZuPUxlz2Lg5dLK PLcJVofmDWtCv0v9IvJLO6T9uM0DDrxdT8LL7MKZdPHGYqQtxoyI5PePIn/OROnXvp2PmuMWNyfh XUYgTbsf+LE3aM+4qPlmpz9nEbw38nKhnB5vYo76yksxex3Eb2ZT1Rcq6mIpSvPmDx4071zW8Juu rfbybz5/zkLo2mepZeWUXVL4VU3jV+qof8mlGl+ii+5zYYOz5S3nsPtaJ5wOTwLzF5o17zFfG+1m 9kvJ9+HM0RAf2Y0FFQeyjNvjxRgKiKcaUieaM0LyXqmp8ZZB9VtDv6sg+Jh/kL1PzO2V5dRGPmW/ FppS8gzq08neXre0NsbVZ+X25ZfikJ8Q23H/AGNMwZaiZN258dNACqSHWPy4UhpdKloev1eY7f7F /wCv35fj1f8AOcfJov5qTaJ5l83+StRP1OWS0etZbST4oJe3xJ9lv9Yb+By/Jix5hvu4lyxnue9/ l/8AnDoXmgx2N2Bp2tNsLZzWOU/8Uue/+Sd/CvXNLqdDLHuN4uXjziW3VKfzI/JKw1hZdT8uolnq 27yWgokE5707RufHoe9OuW6XXmO094scunB3HN5N5J89ecfy18zPcWJe3njYR6lpdwGEUyrX4JU2 3FfhYbjtm8hMEWOTgyj0L7a/Lf8AMvy55+0IanpEnGaPit/YSEetbyEV4sO6n9lxs3zqBcDbURTL MKHYq7FXYq7FXYq7FXYq7FUl84+b9E8o+XrrXdZm9Gzthso3eSRtkijX9p2PT7zsCcBKgPhbz957 8z/mX5uF7cRu8krehpelwlnWGMn4Y0Hdm6u1Nz4CgFM5gCzybox6B7V+V35VWflW1S/1BUuNflWr ybMtuCKGOI+O/wATd+g268/q9YchofT97sMOHh3PNjH5o/nX9XebRPK8oM6kx3eqLuFPQpB4nxft 28cyNJoL9U/k15tR0i8r0DypqOuTG6nZo7VmLSXL1LyMTU8a/aJPUnNjlziGw5teHTme/R6Ppej6 fpcHo2cQjB+2/V2I7s3fNdPIZGy7THjjAUEbkGbTMqKWYhVG5Y7ADFUh1Dzv5fsyVExuZB+xAOX/ AAxov45fDTTPk489VCPmwPzFremapKZYNOFrNWpnD7v/AKygcf4++Z2LHKPM26/NljPkKSXL2hFr q2qrpzaYt5ONOdxK9kJG9EuP2jHXjX3pkeAXdbps1SEySE20DV9O02f1rnTxeSA/A7PTh8lIK1+e U5cZkNjTdiyRibItn2n+e/L92QrytayH9mcUH/BCq/ecwZ6aY83YQ1UD5J/HJHIgeNg6NurKagj2 IzHIcgG12KULqOmWOo25gvIVlj7V6qfFSNwclCZibDCcBIUXnHmPybe6SxurUtPZA19Qfbj8OdP+ JD8M2OHUCex5uszaYw3G4ei/lf8AnbJE0Oi+api8RolrqrmrKegWc91/y+3fxGDq9B/FD5fqZYdR 0kzz8yfyy03zhYG4t+FvrcSf6LefsyAbiOWnVT2PVfwOHpdWcRo/S3ZcQkPN4L5W8z+bPy185C9t la11GycxXtjLURzRn7UUgB+JGG4I9mGdDCYIscnXSjWxfc/5f+fND88eW7fXNIf4JPgubZj+8gmA BeJ/cV2PcbjLwbaiKZHhQ7FXYq7FXYq7FXYqsuJ4LeCS4uJFighVpJZXIVVRRVmYnYAAb4q+Gvz1 /Nu4/MDzMUs3dPLenM0emQGo9Q9GuXU/tSfsg/ZXbryrVI22RFPQvyc/LFPL9kmt6tD/ALnLpaxR uN7aJh9kDtIw+14dPGug12r4zwx+kfa7HBi4dzzY5+cv5sFmn8saBNRVPDUr6NupH2oIyO3Zz9Hj l+h0f8cvgGvPm6BgHk/yd+kON/fqRZA/uouhkIPU/wCT+vMzUajh2HNOm03FueT0dEREVEUKigBV UUAA6AAZrnZAN4pYvr3nvT9PLQWYF3dDYkH92p92HX5DMnFpjLc7BxMuqEdhuWBarr+q6o5N3OWT 9mFfhjHyUfxzPhijHk6/JllPmUvAJIAFSdgBljWzfy/+Sf5qa8ofT/Ld2ImAKzXSraIVP7StcGLk P9WuHhKOIM6sP+cQvzLnQPc3ul2df91vNM7jp/JCy/8ADYeBjxpp/wBCa+av+pgsf+Rc39MPAvGl t9/ziB+ZEKs9rqGl3YFeKCWeNz9DQ8f+GwcC8bCNf/Ir82NCUyXnly5lhFT6tnxvF4jqx+rmRlH+ sBgMSniDBHR43ZHUo6khlYUII6gg4GSN0zW9T0yTnZztGK1aPqjfNTtlc8cZcw2QyyjyLO9B8/2V 4Vg1AC0uDsJK/umPzP2fp+/MHLpSNxu5+LViW0tiywEEVG4PQ5iuY0QGBBFQdiD0IxV5/wCcfJgt lfUtNT9wKtcW4/Y7ll/yfEdvl0z9PqL9MnXanTV6osq/J382W02SLy7r0xOnyHjY3sjf3BOwjcn/ AHWex/Z+XSjXaPi9cefVrwZq2LPfza/LSHzTppv7CMLr1oh9Eig9dBv6Tnx/kJ7+xzC0Wr8M0fpL dmxcQsc3kf5PfmfqX5debVuZBI2k3LCDWrDcEoDTmqn/AHbEalfpXaudFGTrpB93adqFlqVhb6hY zLcWV3Gs1tOhqrxuOSsPmDlrUiMVdirsVdirsVdir51/5yu/NJ7Cxj8i6VNxur5BNrMiHdLcn93B UdDKRyb/ACadmyEiziHlv5F/l8uq6h/iTUY+Wn2D0so2G0twu/Kn8sfX/W+RzU9oanhHAOZczT4r Nlm350/mOdA079CaZLx1i+Q+rKp+K3gOxb2d+i+G58MxNBpeM8R+kfa3Z8vCKHN84ZvnAT7yv5qu dGmET1lsJGrLF3X/ACk9/bvlGbAJ+9yMGcwPk9Pj1GykshfLMptCvP1q0XiOtc1hgbrq7UTFX0ee eaPO1xfl7SwJhsd1aQbPKP4L7ff4ZsMOmEdzzdbn1RlsOTF4opJZEiiQySSEKiKCWZiaAADqTmU4 j3b8t/8AnFPzLraRah5tmbQtOejLZKoa+dT4q3wwf7IFvFckIMDJ9I+Tfyq8g+T0Q6Fo8MN0oob+ QetdN4/vpOTivcLQe2TAYksswodirsVdirsVY15v/LXyP5viKeYNIgvJSKLd09O4WnTjPHxkA9uV PbAQkF84/mP/AM4ma3piS6h5LuW1e0WrHTJ+KXar/kOOMcvyop8A2RMGQk8Buba5tbiS2uYnguIW KTQyKUdHU0ZWVqEEHqDkGbIfLHnO60tktromew6AdWjHint/k5jZtOJbjm5WDUmGx5PSYr6zlsxe RzIbUrz9atF4jqST0pmuMSDXV2YmCL6POfN3nCTUnazsmKaepozDYykdz/k+A+/Nhg0/DuebrdRq OLYcmL5lOI+gfyO/Mg6nar5Z1WWuoWqV0+ZzvNCo/uzXq8Y6eK/I10faGl4Txx5Hm5uny3sUl/Pn 8vlt5T5s02OkMzBdVjUbLI2yzU/yzs3vQ9zl3Z2pv0H4MdRi/iDMf+cT/wA0mEj+QdVmqrc59Cdz 0Iq81sPnvIv+y9s3MS4Mg+ncmwdirsVdirsVSnzZ5k0/yz5b1HX9Qalpp0LTOK0LkbJGtf2nchV9 ziVD4DL675/88tJO/qaprl0XlkoSqBjViB/JFGNh/KMxcuQQiZHo3wjZoPpi9utF8ieSy6rxsNKg CQxVAaR+irX+aRzufcnObiJZsnmXZkiEfc+Udb1i/wBZ1W61S/k9S6u3MkjdhXoq+CqNgPDOlxwE IiI5B1kpEmygsmh2Kqq3l0tq1qJWFs7B2hqeJYdDTI8Iu+qeI1XRHeW/LeteZdattG0a2a71C7bj FEvQDuzHoqqN2Y7AZIC2JL7O/J78hPL3kO3i1C9Can5pZay37CscBYfElsrfZA6cz8TewPHLQKay beqYUOxV2Ksf83ef/J/lCGCXzFqSWIuWKwIUklkegqSI4lkfiO7UpkJ5Ix5lydNo8uYkYxdfjqxj /oYn8nf+pg/6c77/AKoZX+Zh3uX/ACLqv5n2x/W7/oYn8nf+pg/6c77/AKoY/mYd6/yLqv5n2x/W z7TdRs9T0611Kxk9ayvYY7m1loy84pVDo3FgrCqsDQiuXA2LddOBhIxPMGkThYOxV5x+bX5IeWPz BtGuHUaf5iiWlrq0a7tQbR3CinqJ/wAMvY9QQRaQafFfnDyfr/lHXrjRNctjb3sG4I3jkjP2ZYn/ AGkamx+g0IIyohsBSxb+8WzeyWZhayMHeEH4Sw75HhF31Z8ZquihkmLsVRGn6headfQX1nKYbq2d ZYZV6hlNRkZREhR5JBo2+sPLOt6V558mpcSxK8N7E1vqFr1CSU4yJ496qetCDnNZccsOSu7k7KEh OL5q1zTdX8i+dXit5mivdLuEnsLsbEqpDwyDtuKVHjUZ0ODKMkBIOuyQ4TT7x/L/AM42XnHyfpnm G1oovYgZ4Qa+lOnwzR+PwuCB4ihzLBaCGQ4UOxV2KuxV82f85g+d2itNK8mWr0a5/wByOpAf77Ql LdD7M4diP8lchMs4hiH/ADjv5UCwXnme4T45CbSwJ7KKGVx8zRQfY5o+0824gPeXP0sOqUf85Beb 2vNYg8t20lbXTwJrwDo1y4+EH/UjP3sfDLezcFR4zzP3MdTOzTyPNo4rsVdiqK0vS9Q1XUbbTdOg a5vryRYbaBN2d3NABir7m/Jj8n9K/LzQQGCXHmK9RTqmoCvjyEMVfsxp/wAMfiPYC0Cmom3ouFDs Vdirzv8ANr85dE8hWJgTjfeYZ0raacDsgPSWcg1VPAdW7dyKM2YQ97s+zuzJ6g3ygOZ/U+O/Mvmb W/MusT6vrN011fTn4nbYKo6IijZVXsBmtlIyNl7fBghiiIwFAJXkW52Kvvn8tf8AyXPlX/tj2H/U LHm4xfSPc+c67+/n/Xl97I8m4rsVdirCPzY/KrQ/zC8vtZXYEGqW4ZtL1ICrQyGlQafajfiAy/SN wMBFpBp8I+YNB1Xy/rV5o2rQG21CxkMVxE3YjoQe6sKMrDYjcZUQ2Apfil2KuxV6f+Q3m86V5mOj XMnGx1eiRg9FuV/uz7cxVPc0zXdo4OKHEOcfucjTzo13sy/5yE8qC70a38x26f6RpxEN2R1NvI1F J/1JD/wxzE7MzVIwPVt1MLFo7/nD/wA7tBqWp+Tbl/3N2p1DTgegmjASdB/rx8W/2JzfwLr5B9TZ Ng7FXYq7FX5+fmp5ml85fmXrGp25M0d1dfV9PA/ahipDBTt8SqG+ZymR6tsQ+i7KCy8meR0jeht9 Gsy8pG3N0Us5HvI9fvzl5E5cn9Yu1FQj7nyVqWoXOo6hc3903O5upXmmbxZ2LH9edNGIiAB0dYTZ tD5JDsVdir6q/wCcUfysSz08+fNVhreXgaLREcbxwbpJPv8AtS7qv+TXs2WRDXIvovJMXYq7FXkP 51/nrbeTVk0LRQtx5ndAXZhWK0V1DK7gijuVNVTp3bbY42fPw7Dm7vsvsk5/XPbH975J1HUb7Ur6 e/v53ury5cyT3ErFndj1JJzXE3uXs4QEQIxFAIfAydirsVffP5a/+S58q/8AbHsP+oWPNxi+ke58 5139/P8Ary+9keTcV2KuxV2KvB/+cpvytTXPL3+MdNi/3LaLH/p6qN5rIGrMf8qCvL/V5eAyMgyi XyFlbY7FXYqvt55reeO4gcxzQsskUi9VZTVSPkRgIsUVBfXOl3Vl5z8jxSygehrFmY7hRvxdlKSA f6jg0+WcxMHFk/ql2gPHH3vmnyprF55H/MOx1CSqz6Jf8btF6lI3Mc6D/WTkudPCYIBHV1co9H6G wyxTRJNEweKRQ8bruCrCoI+Yy9pXYq7FWKfmt5hPl78uPMOro3CaCykS3cbUmmHown6JJFwHkkPi j8n9GGq/mBpiOtYbRmvJe/8AcDkn/JTjmv1uThxHz2crBG5B65/zkDrZsvJsWnI1JdUuFRh/xVD+ 8f8A4cIM1nZuO8l9zlamVRrvfOGb5wHYq7FWRfl75RuPN/nPSfLsNVF9OFuJFpVIEBeZxXaqxqxH vhAtBL9DbGytLCyt7GziWC0tY0gt4U2VI41Coq+yqKZa1K2KuxV2KvmD/nLPywLfXdI8yQpRL+Fr S6YdPVtzyQt7sj0/2OYGrjuC9b7O57hLGehv5/j7XgWYb0bJl/L3zEvkmbzlcw/VtHWWOC1aWoe4 aRqco1p9habt47Cu9LPDPDxdHE/OQ8XwhvL7mM5W5bsVffP5a/8AkufKv/bHsP8AqFjzcYvpHufO dd/fz/ry+9keTcV2KuxV2KrZI45Y2jkUPG4KujAFWUihBB6g4q/Pz83PJDeS/P8AquhopFksnr6a xqa2s3xxCp3JQHgT4qcqkN20Fh+BLsVdir6B/wCcdNbNxoGo6O7VewnE0QP++7gdB8njY/Tmj7Ux 1IS73N0stiHn/wCeejDTvP8AcTIvGLUoo7tadORBjf6S0ZJ+eZ3Z+TixDy2adRGpPrb8h/MJ178p /L127cp7e3+pTV3PK0YwAsfFkRW+nNnHk4cubPsKHYq8T/5y31c2f5Yw2Kn4tT1CGJ1/4riV5if+ DjTIy5Mo83jn/ONunB9S1rUiN4YYrZG/4zMXYf8AJFc0vaktohz9KNyUF/zkXqZn80afpwNY7O09 Qjwedzy/4WNcn2XCoE95Y6o708mzZuM7FXYq+j/+cOfK6zaprvmeZK/VYksLRiKjnMfUmI91VEHy bJwDCZfU2TYOxV2KuxV53+fvlka/+WGqqic7rTQNRttqkG3qZKfOEuMp1EbgXZ9j5/D1Ee6W3z/b Tyb8lv8AnHiXUPQ8x+coDFYbSWWjuCrzdw842Kx+CdW70H2sbBpr3k7ntTtnhvHiO/WXd7noX/OT MUcX5TyxRII4o7q1VEUAKqgkAADoBl2q+h1vYRvU/Avj7Na9s7FX3z+Wv/kufKv/AGx7D/qFjzcY vpHufOdd/fz/AK8vvZHk3FdirsVdirsVfNn/ADmR5WR7HQvNUSgSRSPpl23crIGmg+hSkv8AwWQm yiXy7kGx2KuxV6X/AM4/6mbXz0bQn4NQtZYuPi0dJgfoWNs1/aULx33FyNMakyj/AJyS04G20TUl FCjzW0h7nmFdPu4NmN2VLeQbNUOReh/84c6uZ/Jmt6UzVNjfrOo3qFuYlUD5coGObyDr5vf8mxdi r5p/5zPviLbypYg7O97O6+HAQqpp/s2yE2cEm/5xytQnlPUboijTXxSu+6xxRkfi5zn+1D6wPJ2O lHpLy/8AOe7Nz+ZGrmtViMMS96cIEBH/AAVc2OhjWIOPnPrLCczGl2KuxV9tf84taQth+UNjcAUb VLq6u3Heok+riv8AsbcZZHk1S5vW8kh2KuxV2KuIBFD0xV2KvJv+cnv/ACVc/wDzGW3/ABI5j6r6 Hc9g/wCMD3F8eZrHt3Yq++fy1/8AJc+Vf+2PYf8AULHm4xfSPc+c67+/n/Xl97I8m4rsVdirsVdi rzf/AJyK0gan+T+voFDS2qRXcRIrx9CZHcj/AJ58hgPJI5vhLKm12KuxVlH5X3htPzB0KUGnK6WG taf3wMVPp55jauN4pe5swmpB7Z/zkBaibyCJaVNteQy132qHj/5mZqezTWX4OXqR6UP/AM4Z3xTX fM1jXae1t5+Pj6MjrX/ktnRQdbN9U5Ng7FXyp/zmZM51/wAtQmnBLS4ceNXkUH/iAyE2cEZ+QMYT yArA7yXc7H5/Cv8AxrnOdpH978HZ6b6Xh35lSNJ5+15m6i8lXbwU8R+AzcaUfuo+5w8v1FjWZDW7 FXYq+/fyPtRbflL5WjFKNYpL8IoKykyH6fj3y0cmo82cYUOxV2KuxV2KuxV5N/zk9/5Kuf8A5jLb /iRzH1X0O57B/wAYHuL48zWPbuxV98/lr/5Lnyr/ANsew/6hY83GL6R7nznXf38/68vvZHk3Fdir sVdirsVY1+ZsAn/LfzVCQDz0i+A5CoDfVn4n6DviVD878pbnYq7FU38nyNH5u0SRftJqFqwr0qJl OVZx6Je4sofUH0Z+d0Yf8tNVYneNrZh8/rMa/wDG2aHQH98Pj9zn6j6CxP8A5w7mcfmLqsIpwfR5 XPjVLq3A/wCJnOlhzdZJ9fZY1uxV8rf85mQU13yzPy+3a3KcadOEiGtffnkJs4Ij/nH+Qv5B4kU9 O8mUe+yN/wAbZznaQ/e/B2Wm+l4j+ZkRi8/68pNa3kj/APBnl/HNxpD+6j7nEy/UWM5kNbaozGig sRuaCuBWsKvvn8ibwXn5ReV5QahbP0a0K/3EjRUoadOHXvlo5NR5s8wodirsVdirsVdiryb/AJye /wDJVz/8xlt/xI5j6r6Hc9g/4wPcXx5mse3dir75/LX/AMlz5V/7Y9h/1Cx5uMX0j3PnOu/v5/15 feyPJuK7FXYq7FXYqxf807kW35aea5ieJGk3oUgV+Jrd1Xb/AFiMBSH55ZU2r2hmVA7RsqMKqxBA I9jgsJorMKE48mRet5w0KKtPU1C1WvWlZlFcqzmscvcWWP6h730V+d8hT8tdUUCvqPbKfb/SI2/4 1zQ9nj98Pj9zn6j6Cxb/AJw7gr+YWrz8vsaRInGnXncwGtfbhnSwdZJ9eZY1uxV83f8AOZ1iz6Z5 Wvx9mCa7gY+8yROP+TJyE2cGO/8AOOF2G8s6pad4b31T8pYkX/mVmg7Uj6wfJ2OlOxeb/nbZm2/M fU2p8NwsEyfIwop/4ZTmfoJXiDj6gessn0jTtIfT7W4jsoFMsKOWWJATyUHwzFyTlZFl2eOEeEGg mXpR+mYwoCEEFRsN/lldttPELiFoLiWB/tROyN81NDm6BsW6IijT7N/5xR1kX35UR2ZI5aTe3NqF 78ZCLkH6TOctjyapc3smSYuxV2KuxV2KuxV5N/zk9/5Kuf8A5jLb/iRzH1X0O57B/wAYHuL48zWP buxV98/lr/5Lnyr/ANsew/6hY83GL6R7nznXf38/68vvZHk3FdirsVdirsVeW/8AOTGsppn5Qauh YLNqL29lBXuXlV3H/IqN8EuSY83w5lTa9s023Ntp1rbnrDDHGfmqgZpZmyS72AqIC27tNOMMktxb RSKilm5orbAV7g4xkehRKMa3DAvyptGvPzF0SMDdbgzGm39yjS/8aZsNZKsUvc6jCLmHsf8AzkJd CHyNDDX4rm+iSnsqSOT/AMKM1XZgvJ8HK1J9Lf8AzhjYltS80X9DSGG0gB7EyvK57dvSzooOtm+p Mmwdirxv/nK/RzfflQ94q1bSr63ui1N+LlrYivhWcZGXJlHm8N/5xv1IRa7q2mk0+tWyTqPEwPx/ VNmn7Uh6Qe4udpTuQ3/zkfpZi1zStUC/Bc2z27EfzQPy3+ib8Mey5+kx811Q3BQ3ke7+s+XLYE1a AtC3+xNR/wAKRg1MamXN0srgE+yhyHk3nWx+qeYrmgok5E6e/P7X/D1za6eVwDp9TGpl7L/zh95q jsvNWq+W534rq9utxagnrPaciyqPFopGb/Y5lQLizD60ybB2KuxV2KuxV2KvJv8AnJ7/AMlXP/zG W3/EjmPqvodz2D/jA9xfHmax7d2Kvvn8tf8AyXPlX/tj2H/ULHm4xfSPc+c67+/n/Xl97I8m4rsV dirsVdir5f8A+cx/NavdaF5UhkqYVfUr1BuOT1ht/pAEu3gRkJlnAPnry9Ym+1qztqVVpAX/ANRP ib8BlGWXDElvwx4pAPZc07u0o823f1Xy7eyVozx+kvzkPD9Ry3BG5hp1EqgUH/zjzpZufONzflax 2Fo5DeEkzBFH0pzy3tOdYwO8uu0w9Vp3/wA5JakC+iaYp3AmuZV+fFEP4PlXZUPqLPVHkHqP/OH+ jm28g6nqbrR9R1BlQ06xW8aKDX/Xd83cHAk93yTF2KpB+YHl7/EfknXNEC8pL6ymigB/39wJiP0S BTgKQ+CPI/mmTyr5nttY9EzLAJEltgeBdXRl4kkGnxEHp2zD1GHxIGLkY58JtFeefzC1/wA4XCPf lI7KBma1s4hRY67VYn4malNyaeFMjp9NHENuacmUy5pLpOu6npMvqWcpVSavE26N/rL/AB65bkxR lzXHllA7PQdC886ZqHGG5pZ3R24uf3bH/Jf+BzAy6aUeW4dji1UZbHYoP8x9LM1jDqMYq1seEpH8 j9D9DfryeknRrvYayFji7mIeU/Md95Z8y6br9if9J06dJ0WpAcKfjjYj9l1qrexzYAusIfoj5f1z T9e0Sx1nTn9Sy1CFLiBu/FxWjDsy9GHY5c1I/FXYq7FXYq7FXk3/ADk9/wCSrn/5jLb/AIkcx9V9 Duewf8YHuL48zWPbuxV98/lr/wCS58q/9sew/wCoWPNxi+ke585139/P+vL72R5NxXYq7FXYqh9R 1Cz06wudQvZRBZ2cTz3MzdEjjUs7H5AYq/PP8wfN915w85ap5iuAV+vTFoIm6xwIAkMe23wxqoPi d8qJttApNvy20stLcam4+FB6MJ9zu5+gUH05g6yfKLsNFj5yT7XfOelaXyiRvrV2NvRjOyn/AC26 D5dcx8WnlLyDkZdTGHmXn2teYtU1d63UlIQapAmyL9Hc+5zYY8UYcnW5c0p80w8k+fdd8n3sk+mF HhuOAu7WVapKErx3FGUrzNCD9+Q1GmjlFFGPIY8l35h+c283+Yf0r6Jto1gigigZuZQIOTDlQVHq O1NsdNg8KHCuXJxG327+Tnl0+Xvyx8u6Y6cJltEnuF7iW5JnkB91aQjM0cnGPNmWFDHfOn5g+UvJ en/XvMOoJaqwPoW4+OeUjtFEvxN8+g7kYCaSA+WfzN/5yh81+ZPW07y0H0HRnqhlVh9dlU/zSLtF X+WPf/KOQMmYi8h0TQtb1/Uo9O0ezm1C/mPwwQKXY77s1Oiiu7HYd8iyfSn5Zf8AOJltB6WpefJh cTbMuiWzkRr7TzLQv/qx0H+URkxBgZJz+ZP/ADin5a1lZL/yfIuialQk2T8mspDToAKvCT4rVf8A JwmKBJ8v+bfJPmnyjqR07zDp0tjcbmNnFY5FH7UUi1Rx/qn55WQzBW6b5pv7S3ezn/0vT5VMb28h 3CnY8G6r7dsonhBNjYuRDOQKO4SZuPI8SStfhJ2NPfLmh9Gf84qfmuljdHyHq8wW2u3Muhyudknb eS3qdgJPtJ/lVG5YZZEsJB9UZNg7FXYq7FXYq8m/5ye/8lXP/wAxlt/xI5j6r6Hc9g/4wPcXx5ms e3dir75/LX/yXPlX/tj2H/ULHm4xfSPc+c67+/n/AF5feyPJuK7FXYq7FXzV/wA5W/mskcA8gaTK DLJwm12VD9lRR4rb5ts7+3EdzkJFlEPl/INicz+Z74afFptiTaWcS8W4GkkhO7MzDxPYZSMIviO5 bjnPDwjYKflryr5i8zammmaDYS6heP8A7riWoUfzO5oqL/lMQMuaX03+Wf8AzidpGnejqXneZdTv BRl0mAkWqHrSV/haUjwFF/1hkxFgZIf8zP8AnEywu/W1LyLMLO4NXbRrliYGPWkMpq0fsr1HuoxM VEnzRr3l3XPL2pSaZrdjNp99F9uCdSpp2ZT0ZT2Zag9sgzer/lp/zk15u8rGGw1zlr2hiiqJW/0u FRtSOY15hf5ZPvXJCTExfU/kf8yPJ/naw+t+X79J3UA3Fm/wXMNdv3kR+Ib7chVT2JyYNsCKfn7r GtavrWoS6jq15LfX05rLcTuXc+1T0A7AbDKm17J+Vn/OL/mDzNDbax5lmOj6HOqywwx8XvJ42AKs o+JIlYHZmqf8nvkhFiZPqbyf5F8qeT9OFh5e06KyiIHqyKOU0pFfillarudzSp27UGTAYEp2J4SK iRafMZDxYd4+a0XetD/Ov3jHxod4+a0Uv17Q/LvmDTZNM1u1t9QsZftwTgMK9mU9VYdmG4x8WHeP mtF80/mZ/wA4pz2vral5Euhd2+7Po1zIomQdf3MxIWT2V6GndjlZnDvHzbAS+dp4JoJpIJkMc0TF JI2FGVlNCCPEHCClqKWWGVJYnaOWNg8ciEqyspqCCNwQcKvs78gPzyt/OunpoWuSrF5qs4wObFVF 7Go/vYxt+8UD94o/1htULZE21EU9lySHYq7FXYq8m/5ye/8AJVz/APMZbf8AEjmPqvodz2D/AIwP cXx5mse3dir75/LX/wAlz5V/7Y9h/wBQsebjF9I9z5zrv7+f9eX3sjybiuxV2KvK/wA8vzssPIOk tY6e6XHmm9RhaW4KsLdSKC4mXfYV+BSPiPtXATSQLfEd3d3V5dzXl3K891cO0s88hLO8jnkzMx3J JNTlTapYq+gPyy/5xR1vVRFqXnWV9IsGo6aZFQ3kgO/7wnksI9t27EKckIsTJ9O+WPKXlryrpa6b oNhFp9mm7LGPicgU5SSNV3b/ACmJOT5MCbTX1of51+8ZDxod4+a0XetD/Ov3jHxod4+a0Uk82eUP KPm3TTp3mCygv7ff0y9BJGT+1FIpDxn3U4nLA9R81AL5f/Mz/nF3W9DSfUfKVz+mtKQF2spCq3sQ G+1KLMKD9ni3+SeuVmcO8fNsBLxLS9V1PSr6K/0y6lsr6A8obmB2jkU9NmUg5JKFxV+jHkP/AJQb y7/2zLP/AKh0y0NJTa8lCRFf2n2+jvmPqsvDGhzLKItL81Dc7FXYql+sXfpQeip/eS9fZe/39Mpz ToU3YYWbfAXmT/lIdU/5i5/+TrZvMf0j3NMuZTjUvKTvoVnq1gvIm3RrqEdfs7yL/EfTlMM/qMT3 uTPT+gSHcxu0u7qzuobu0me3urd1lgniYo6OhqrKwoQQRscynEfWP5N/85OabrUcGh+dpo7DWAAk OrNxjtrkj/fvRYZD/wAAf8nYZYJNZi9/BBFRuD0OSYuxV2Koe/07T9QtzbX9rDeW5IYwzxrKlR0P FwRtgIB5soTlE3E0Ut/wP5L/AOrBpv8A0iQf80ZHw49wbvzeX+fL5l3+B/Jf/Vg03/pEg/5ox8OP cF/N5f58vmU3gggt4I4II1igiUJFEgCoiKKKqqNgANgBk2gkk2V+KHYq8O/OT/nJTR/LEc+i+VZI 9T8xbxy3QIe1tG6GpG0sq/yDYH7XTiYmVMhF8i6rquo6tqNxqWpXD3d/duZbi4lNWdj3P8B2ytsT bR/K01zpd5qlyDHawwSvAvQyOqEg/wCqD9+Y+TOBIRHO3Ix4CYmR5Ux/Mhx36a5c0oS/looiH7W7 H28PpzD1mWhwjmWcAg81ba7FXDCFSnX7vhbNbofidSX9lp/HKMs6oN2KF7vz4apHL6D8/wC3Ogcd zCh9juMVfov5EIHkXy8TsBpdnU/9G6ZbdC2pEzSmSQt26Ae2aXNk45W3AUsypLsVWu6ohdjRVFSc SaUC2MXNw9xM0r9WOw8B2Ga+cuI258I0KfDfmIj/ABDqZ7G7n/5ONnTY/pHucCXMvUvLX/KP6f8A 8YE/VmrzfWXcYPoHuY95o8ipcl7zSlEc53kttgrnxTsp9umZGHU1tJxs+lveLz+WGWGRopUaORDR 0YEEH3BzPBt15Fc3pP5a/n/568jiOzSUatoaUH6Lu2YhF8IJd2i+W6/5OTEmBi+kPJP/ADk1+Wvm OKKLULo+X9SYASW9+aQ8qb8LoD0+IPd+B9skJBgYl6raXlpeW6XNpPHc28grHNEyujDxDKSDkkKu KuxV2KrJ7iC3hee4kWGGMcpJZGCqoHcsaAYq8w85f85I/lf5bSSOHUP03fqPhtdNpMld6criohAr 1oxI8MBkEiJfOP5kf85G+evOUctjbsND0SQFXsbRyZJEP7M9wQrOO1FCqR1ByBkzEXlQBJAAqTsA MiyZt5X8hySFLzV0KRijR2h2ZveTwH+TmFm1PSLnYNLe8vky/XFVNA1BVAVFtZgqjYACM7DMTH9Y 97m5foPueNHNw6R+mckixoXboP8AOmWSkIiy0gJWzM7F2+0xqc0mSZlKy3gU1kEuxVZLKsUTSN9l QScBNC0gWaYreTPN60j/AGmBP4Zg8VytzgKFPg9SK0PQ7HOpdc3x2p1/lI70xV+hnlCankXy5GOp 0yyLfL6umY2ry0OEMYje0wzXNjsVdiqT61ejl9UQ7ijSfrA/jmNnn0cjBDqlWYzkvh3zB/x3tS/5 ip/+TjZ1OP6R7nWy5vUvK8kcnl6wKMGCwqrUNaMooR9GavMPWXcYD6AmmVNyWaz5c0vV46XUVJgK JcJtIPp7j2OW48socmrJhjPm828y+W5dEuI0adJo5gTERUPRafaXt18c2OHNxh1efD4Z5pNlzSj9 I1/XdGmM+kajdadMestpNJAx+ZjKnG0UzXTf+chPzi09BHD5kmlQU2uYoLkkDxeaN3/HDxFHCEw/ 6Gd/Ob/q9R/9Idr/ANU8PGV4QgdQ/wCciPzjvozHL5jliQ9reG3gI+TxRo/44OIrwhhes+ZfMeuS iXWdUu9SkH2Wu55JiPlzZqfRgtNJbilMdC0SfWL36rDIkbBS7M5/ZBANAOp3yvLkEBZbMWIzNB6T oXlHStJ4yKvr3Y63Eg3B/wAhei/r981uXPKfudpi08Ye9O8pb0v8wui6FqJdgoNtKoJNNyhAH0nL MX1D3teb6D7njWbh0j9KL6WrCMHZd2+eYmsy2eEMYDqhcwGx2KuxVJtYu+UggU/Cm7+7EfwzGzT3 pycMNrSib+6f/VP6sojzDeXwjnVOtbBZTtsR1+jFX0j+WH/OR9kul6doPmILYyWkMdrb6iAWgdYl Eaer+1G3ECp3XvVc1upwZLMo7tuPh5F7VB5hmuIUngljmhkUNHLHRlZTuCrA0IOaw5pDm5HgxX/p m98V+7B48l8GLf6ZvO/E/R/Q4fHkvgxQM0xZnllYCtWdjsB45SSSbbQKFPKvPf5++XND9Wz0QLrG prVeaH/RY2/ypB9ungn/AAQzOwaCU95bD7WmecDk+aL27lvLye7mp6tzI8snEUHJ2LGg+ZzeRFCn DJTDStY1jQblWQMiSqsjW8oISRHAZWofFTUMMrnjjMNuPLKB2ejaF5q0zV1Cxt6N1T4rZz8X+xP7 QzXZcEoe52eLPGfvTnKW94/5n1Y6prM9wprCp9OD/jGuwP09c2+HHwxp0ufJxyJSrLWp2KuxV2Ku xV2KuxVF6TqMmnalb3sfWFgWHip2YfSpyGSHECGeOfDIF7PBNHPDHNE3KOVQ6N4qwqDmnIo07sGx aW635l0vSIz9Yk53BHwWyULn5/yj3OWY8Mp8mrLnjDnzeb6zr+ra9OQwb0UDOlrECVVVBZmNOtFF STmxx4owDrMuaUyk4NDX9eXNL6o8if8AOSmm62sNjrsq6VqzAKbhwv1aV6bkOR+7JPZ9vAnNTqMG WJMh6h9rfDgOxeorq1+yhluKqRUEKhBB/wBjmv8AHk3+DFWGs3tOq/OmPjyR4MWxrV4D+wfmP7cP jyXwIpfNMAHmmcKBV5HY0AHUkk5TuS28nkXn7/nILQtLWWw8uKurX9Cpu6/6JGdxsw3l/wBjRf8A K7ZsdPoJHeWw+1onnA2D5szdOI+5PzL/AOcefJHnX1b2KP8AQ2uvVv0jaqOMjnvPD8Kyb9SOLf5W WGNtYk+UvzA/J/zv5EnZdZs/U012Cw6tbVktWJ+zV/hMbHpxcA+FcgRTMG0D5N/Mrzb5Qlppl1zs yayadcVkt2Pc8KgofdCD45jZtNDJzG7ZDIY8n0L5E/O3yr5o9O0uGGk6u9FFpOw9ORiaAQy7KxO3 wmjeAPXNRn0U4bjcOVDMCmfnf80/KflFGjvrj6xqVKpptvR5qkAjnvSMbg1bt0ByvBpZ5OXLvZTy CL5389fm/wCbPNpkt5Jf0fpLbDTrdiFZa7es+zSfgv8AkjNzg0kMe/M97iTymTD4dL1OexuL+C0m lsbQqLq7SN2iiMhogkkA4ryOwqd8ymtkv5a6x5N0zXVl80WH1u3aggnNXjgav2ngp+8H308DmLqo ZJR9BptxSiD6n0Rr/lPyf540eJ5ljuYWT/QtRtmX1EH/ABXIKinipqPbNFjzZMMv0OdKEZh4P5z/ ACc81+W3e5tEbU9MQ8lurZT6iAb1kiFWWniKj3GbnBroZNjsXDnglFj8fnTXBp01jLKJklQxrK/9 4oOxow67eOXnTxu0jUz4aSHL3HdirsVdirsVdirsVdirsVTqDzfrVvpUenW8gijjqBMB+84k141P Sle2UHBEyst41EhHhCfeT/yn82+apVunjax06Q8n1C6BBcHvGh+KT59PfKs+shj25nuRDDKW73vy t5F8peSdOklhVFcITeapdFeZUfaq5oqJ7DbxrmlzaieU/oc2GOMA8B/NPV/I2o63z8rWXoBS31u7 T93DMx7xw0+Gn821fDvm60cMkY+suDmlEn0sTbTNRWxiv3tZlsZ3aKG7MbiF5EFWRZKcSwruAczG pl3kb83vNvlIpbwyi/0oEV0+5JZVG1fSf7Ue3Snw/wCScxc+khk8i2QymL6G8jfmx5U83IkNtP8A VNVI+PTbghZKgVPpH7Mg2P2d6dQM0+fSTx89x3uXDKJKXnr83/KflJXt5Jfr+rLsNOtyCyn/AItf dY/p+L/JOHBo55N+QRPKIvnfzx+anmvzfI0d5P8AVtMr8Gm25KxbGoMneQ7ftfQBm4waWGPlz73F nkMlPyF+V3nPzzeiDQbFnt1YLcahNWO1ir/PJQ7/AOSoLe2ZQFtRNPqr8sv+ca/JnlL0r/VVXXtc WjetcIPq0Tdf3MBqKg/tvU9xxyYiwMnr+SYqdzbW11byW11Ek9vMpSWGVQ6Op2KsrVBB98VeBfmb /wA4o6Jqnq6l5KlXSb41ZtMlJNpIetI23aEk9t19lGQMWQk+YvM/lLzH5W1N9M1+wlsLxdwso+F1 /mjcVR191JGQIZ2hNL0nVtZ1COx0y1mvr+4akcEKtJIxPegr9JxS+jfyy/5xLJ9HUvPs1Bsy6Hav v8ridf8AiMZ/2XbJiLAyfRlh5d0HT9HGi2WnW8GkBGiNgkSCEo4owZKcW5ftV698mwfOf5uf84rM Gn1ryCKqayT6DI247n6q7f8AJtj8j0XIGLMSeG+XfN/nDyPqcsNu0lq8blbzS7pWCFhsRJE3Eq23 UUOYufTQyD1BuhkMeT3Hyf8Anf5V1wR2+ot+iNRagKTkegzf5E2wH+zp9OabP2fOG49Qc2Goieey N83/AJR+UPNDm94Gxv5BX65acQJOpBkSnB+v2tmPjkMGtyY9uY80zwxk8l8wfkF5z04tJpxi1a3H T0j6U1PExyGn0KxzZ4+0ccufpcaWmkOW7ANS0bV9Ll9LUrKezkrQLPG0ZPy5AVzNhkjLkbaDEjmg 8mh2KuxV2KuxVFafpWp6jN6On2k13N/vuCNpG39lByMpiPM0kAnkzzy/+RHnfU+Ml6kWk27blrhu UtPaJORr7MVzCydo448vU3R08jz2es+UPyX8o+XZEvJlbVNQj3E90B6aHxSIfCP9lyI8c1mfXznt yDlQwRj5u84fnP5S8v8AqW9vJ+ldSWo+r2xBjVh2kl3UfJan2xwaGc9zsFnnjHzeFea/P3mzzneJ BcyM0LuBa6XbBvT5E0UBBVpH32JqfDN1g0sMfLn3uFkymXN7F+Uv/OK97emHWPPga0s9ni0NDxnk HUfWGH92v+QPi8eOZYi0GT6Xl8teXptDGgy6bbPoojEI04xIYBGvRRHTiKdsnTB89fmb/wA4lxv6 upeQpuDbs2iXT1U+1vO3T/VkP+yyBizEnzfq+jazoWpyafqtpNp+oW5+OCZTG6nsRXt4EZAhmv0L y/rvmHU49N0aym1DUJz8EMKlm92Y9FUd2Y0HfEBX0r+WX/OJllbCLUvPcwu59mXRbZyIVPWk8woz nxVKD/KYZMRYGT6H0/TrDTrOKx0+2itLOBQkNtAixxoo7KqgAZNgiMVdirsVdiqVeZfKvl3zPpj6 Zr1hFqFm/wDuuVd1JFOUbijI3+UpBxpbQPkv8u/J/kuyNr5e06O05/31wavPLvX95K1XYeArQdhg ApJLI8KHYq7FWG/mD+Unkjz3b8dbsgL5V4wanb0juox2HOhDgfyuGHtgItINPmPz/wD84t+fPLzS XWhD/EWmLVh9XXjdoPBrckl/+eZYnwGQMWYk8+0Lzx528pXBtrS7nthC1JdOuAWjBrUqYpPsHxpQ 5jZdNDJ9QbYZZR5F6ZoP/OR8RCx6/pRVtuVxZNUf8ipCKf8ABnNdk7L/AJp+bkx1XeGdaf8Amp+X GsxemdVgQOKPBegwj5H1QEP0E5hS0eWHT5NwzQPVXl8h/ltrSeqmlWE6NuZbUKla9+UBXANRlh1P 496fDgeiVT/kX+XMpqljNBvWkdxKfo+NnywdoZe/7GB08EKf+cf/ACD6nP8A0wLWvp+sOPy+xy/H Jfyll8kfloomD8ivy6jNXs5phWtHuJQPl8BTAe0Mvf8AYn8vBNYfy9/LbR09ZtIsYkX/AHZdgSAU /wAqcvlR1OaXU/D9jPwoDop3v5mflvokXojVbUKn2YLIesK+AEAZR9OGOkyz6H4oOWA6sH17/nI6 yQNHoOlvM/7NxeMEUH/jHGWLD/ZjMzH2Wf4j8mmWq7g8y8w/mJ5280yfV7u9kaGY8U0+1Bjibkdl 4Ju/tyrmxxaXHj5Ddx55ZS5s18g/84yfmF5laO41SH/DultQma8U/WGH+RbVV6/8ZOGZQi0mT6d/ Lr8mPI3kOJX0u0+saoVpLq11SS4NeoQ0Cxr7IBXvXJgUwJtnWFDsVdirHvOf5f8AlLznp/1LzDp8 d2qg+jP9ieInvFKtHX5Voe4OAhIKr5R8keVvKGmjT/L2nxWMBp6rqKyysP2pZGq7n5n5YgKSnmFD sVdirsVdirsVdirsVdirsVdirsVdirH/ADV+X3kvzZD6XmDR7e/NOKzuvGdR4JMnGVfobAQkF415 n/5w78tXReXy5rNxprndba7RbmL/AFVZTFIo+fLImDLieX67/wA4rfmxprMbK3tdYiG4a0uFRqe6 3HoGvsK4OEp4gwm//LL8zNGkMlz5c1S3Kf7vS2mZBtX+9jDL0HjkTFIKCbXvPOmHg2o6nYkVXiZr iH3IpyXKjggecR8mYnLvW/4687f9TDqX/SZcf814Py+P+bH5BPiS7y2vmLzxqR9NdT1O9Oy8BPcS /a6CnI9cIwQHKI+SDOXejLL8ufzK1qX1Lfy9qt2z/wC72tp+Jr4yOoX8ctEe5iSzTQv+cWvzb1Jl N1Z22kRGnx3lwhNPHhb+u30EDJcJY8QeneWf+cOdDgKy+ZNcnvWFCbaxRbdPkZJPVZh8guEQRxvZ fKX5aeRfKSAaBo1vZygUN1xMlwfnPIXkp7cqZIBiSybCh2KuxV2KuxV2KuxV2KuxV2Kv/9k= - - - - application/vnd.adobe.illustrator - xmp.did:61e30391-89d4-d941-8455-8d44a928ac5f - uuid:717b9eac-e1fd-4b0c-ad7b-6573570e1b25 - xmp.did:ef0a0210-7abd-1e43-8593-6a0a8a6fa763 - default - - xmp.iid:8e3b6d1a-3d57-2246-a9cd-6e94b90fcfdd - xmp.did:8e3b6d1a-3d57-2246-a9cd-6e94b90fcfdd - xmp.did:ef0a0210-7abd-1e43-8593-6a0a8a6fa763 - default - - - - - saved - xmp.iid:ef0a0210-7abd-1e43-8593-6a0a8a6fa763 - 2022-07-14T11:29:30+02:00 - Adobe Illustrator CC 23.0 (Windows) - / - - - saved - xmp.iid:61e30391-89d4-d941-8455-8d44a928ac5f - 2022-07-14T11:32:11+02:00 - Adobe Illustrator CC 23.0 (Windows) - / - - - - 1 - True - False - - 620.000000 - 620.000000 - Pixels - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - Document - - - - - - - - - - - - - - - - - - - - - - - - - -endstream endobj 3 0 obj <> endobj 6 0 obj <>/Resources<>>>/Thumb 60 0 R/TrimBox[0.0 0.0 620.0 620.0]/Type/Page>> endobj 56 0 obj <>stream -H10 ݧjq@d{mn~{.VVdq z9$MQ "J8fYi6ӓќތ0gZ -endstream endobj 57 0 obj <> endobj 60 0 obj <>stream -8;Z]";%eC%#Xl<)"h@C#MX3fB_.9jdm)/u_7Zl;F:jIfi=ZDg^Lh>==\s& -*Z7^dOFr*:aG/b&Q3G;uM(cG]\R:i=Pr4-f0X!+8lLalKd=F$4T\ -'+6?D-hmP+/E-8!Z6?c8A\kY>:G%eB1>4Y*[Hr'GAUa?^rO7In7ckS_ -=gjPYW*o:;5Ru^Ags!#FUeC2391A~> -endstream endobj 61 0 obj [/Indexed/DeviceRGB 255 62 0 R] endobj 62 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> -endstream endobj 59 0 obj <>/Font<>/ProcSet[/PDF/Text]>>/Subtype/Form>>stream -BT -0 0 0 rg -/GS0 gs -/T1_0 1 Tf -0 Tc 0 Tw 0 Ts 100 Tz 0 Tr 12 0 0 -12 -103.0967 -61.2773 Tm -[(T)7 (his is an A)11.9 (dobe\256 I)-10 (llustr)5.1 (a)4 (t)5.9 (or\256 F)25.9 (ile tha)4 (t w)4 (as)]TJ -0 -1.2 Td -[(sa)8 (v)10 (ed without PDF C)11 (on)4 (t)6 (en)4 (t)3 (.)]TJ -0 -1.2 Td -[(T)71 (o P)5 (lac)6 (e or open this \037le in other)]TJ -0 -1.2 Td -[(applica)4 (tions)10.9 (, it should be r)10 (e)-28 (-sa)8 (v)10 (ed fr)10 (om)]TJ -0 -1.2 Td -[(A)12 (dobe I)-10.1 (llustr)5 (a)4 (t)6 (or with the ")3 (C)3.1 (r)9.9 (ea)4 (t)6 (e PDF)]TJ -0 -1.2 Td -[(C)11 (ompa)4 (tible F)26 (ile" option tur)-4 (ned on. )41 (T)7 (his)]TJ -0 -1.2 Td -[(option is in the I)-10 (llustr)5 (a)4 (t)6 (or Na)4 (tiv)10 (e F)31 (or)-4 (ma)4.1 (t)]TJ -0 -1.2 Td -[(Options dialog bo)14 (x, which appears when)]TJ -0 -1.2 Td -[(sa)8 (ving an A)12 (dobe I)-10 (llustr)5 (a)4 (t)6.1 (or \037le using the)]TJ -0 -1.2 Td -[(S)-3 (a)8 (v)10 (e A)6 (s c)6.1 (ommand)10 (.)]TJ -ET - -endstream endobj 55 0 obj <> endobj 64 0 obj <> endobj 65 0 obj <> endobj 66 0 obj <>stream -H|TiPYjPfPƪTpdP9WdETZmFW4tXDECKQ@DnedËa\'fga̟"22_2c|6ylܰtaU\᥊z2 :Trr8?א?F+PO<2Y-& rd+lݴt,_|Z\~Jau2L-sPEFQJ%4T6 jJ3u{U LT3 HW)(\ BdS1Tp% KLX&DLgR)ܼ}G*e+e -A Å0-1 -Ö`v8ly ?af ;eaXn `L.Rn4bOq1#2$XRj4HnTcjF?&4㍸ 6R $}bxG CÔ1yF2I|;$DOih"Q-V7M tnO9. 'ďLsδN.9`#@?kAtՒuh/#UH]zKHM0S"}RQ5@ա E vBwJeWC ݋B G&f:A@q^'vwYU0J#)_K'7bL*R]7@_`>MK}RvGLueFsd 's4 >N(1Ÿ3߈'k10粶6Qk0ϭlM1͸y&u7ATQǀ6C-A!,yTh~/D p}7D51 e)@Xkǿ -,=Aj/ؘB -CԲuuk΃`4&ppBktvORBA]xǻOF4O5F,ZX_yrLân}(Ģ@By#rx6HK+̫gnreyZ~V{3}OȹaO:Bqz%Ck]fӱ 1iQVWĶ3`ۚQYWIo7m/`Baas4̪s;(7 ;lݞ_`ċ~)FDpt\@N]{RZl7)+J;&ꫪ.,l;rȟ&.(Ϯc>w$hk2R ~j*,b J{f8Tq}Bq/ߓVYt=?9[᝻!$Ƥo*F(-,22- ` Fg@PN&{(3αy-U+$YcWuVNInrT[uc]` n/rL Y0YY98Uףk*CjzrͰDxm iB"4FJl'3xjp?b_~ɭ}Y\.UNXmxK8JN[ZvtTA -FG;9%My2jz.n=s}pPh̑(@)oߟ#rSNgrncU̡a776&h8m=T #PޔPk'(uhXj2W) fI/,=SyNpीML AtH(BڅYlmf{ jI\~)PTJ`UǾp6FF{2Y1M]q4MnAYlV>lX t7E&Pe[6ǧb+F[ǀVQMQ"L!IL# 1-?=N~ܓۿH+,.(#6c;CmS宋rsgstP`a+0w3TJ-j\`2C?{^$/ƣP£0>7W!Feolj1CNB[B"b[KgD{c'%%~Ʌ jOJOeŮ<"N@ 9];zX򋬢]q^daVE(Jyyb_>@[<) ZQ"O;IBAB*n-co.1͒T^1Rei7+]<:z 9tlG$cpS('S9Y(T0u|~*QQf`S'/z/W<Բs=VZ(I,`72Q -Gy0\3Z=! YC!w^7ςMb y_3n& YU =o=S.;Y lseGŃ=/QTUBR~ڿ{M,LKJW!^Md\K3&4M =G ]hkƙ gzc+90 -endstream endobj 63 0 obj <> endobj 58 0 obj <> endobj 67 0 obj <> endobj 68 0 obj <>stream -%!PS-Adobe-3.0 -%%Creator: Adobe Illustrator(R) 17.0 -%%AI8_CreatorVersion: 23.0.1 -%%For: (rr-) () -%%Title: (installer icon.ai) -%%CreationDate: 7/14/2022 11:43 AM -%%Canvassize: 16383 -%%BoundingBox: 656 238 1242 842 -%%HiResBoundingBox: 656.764393457089 238.799999999999 1241.31560654291 841.120025677788 -%%DocumentProcessColors: Cyan Magenta Yellow Black -%AI5_FileFormat 13.0 -%AI12_BuildNumber: 540 -%AI3_ColorUsage: Color -%AI7_ImageSettings: 0 -%%RGBProcessColor: 0 0 0 ([Registration]) -%AI3_Cropmarks: 639.040000000001 229.960037252957 1259.04 849.960037252957 -%AI3_TemplateBox: 959.5 540.5 959.5 540.5 -%AI3_TileBox: 663.402213134767 131.224929831082 1234.72222045899 948.664922735745 -%AI3_DocumentPreview: None -%AI5_ArtSize: 14400 14400 -%AI5_RulerUnits: 6 -%AI9_ColorModel: 1 -%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 -%AI5_TargetResolution: 800 -%AI5_NumLayers: 1 -%AI9_OpenToView: -9258.43684210526 7167.7052631579 0.109953703703704 2255 1454 18 0 0 78 121 0 0 0 1 1 1 1 1 0 1 -%AI5_OpenViewLayers: 7 -%%PageOrigin:936 516 -%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 -%AI9_Flatten: 1 -%AI12_CMSettings: 00.MS -%%EndComments - -endstream endobj 69 0 obj <>stream -%%BoundingBox: 656 238 1242 842 -%%HiResBoundingBox: 656.764393457089 238.799999999999 1241.31560654291 841.120025677788 -%AI7_Thumbnail: 124 128 8 -%%BeginData: 20057 Hex Bytes -%0000330000660000990000CC0033000033330033660033990033CC0033FF -%0066000066330066660066990066CC0066FF009900009933009966009999 -%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 -%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 -%3333663333993333CC3333FF3366003366333366663366993366CC3366FF -%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 -%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 -%6600666600996600CC6600FF6633006633336633666633996633CC6633FF -%6666006666336666666666996666CC6666FF669900669933669966669999 -%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 -%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF -%9933009933339933669933999933CC9933FF996600996633996666996699 -%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 -%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF -%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 -%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 -%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF -%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC -%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 -%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 -%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 -%000011111111220000002200000022222222440000004400000044444444 -%550000005500000055555555770000007700000077777777880000008800 -%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB -%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF -%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF -%524C45FD3DFF27277DFD76FFA852F820F8F827A8FD73FF7D27F826F827F8 -%20F87DFD70FFA827F8F827F820F826F820F8277DFD6CFFA852F820F82720 -%27F826F827F827F82027A8FD69FF5220F826F826F8F8F87D7D27F820F826 -%F8F8F852A8FD65FF7D2620F827F827F82051FFFFFFA87DF820F8272026F8 -%2776FD62FF7D51F8F8F826F8F8F8277DFD06FFA827F8F826F820F8F8207D -%FD5FFF5226F827F827F820267DFD0AFF7D51F826F8272026F852A8FD5BFF -%A8F8F8F8272020F82652FD0EFF7D20F8F827F820F8F87DFD5AFF7D26F827 -%F827F852A8FD11FF5226F8272027F87DFD5AFFA8F826F82027A7FD0AFFCF -%FD09FFA851F826F8F852FD5AFFA827F827F8A8FD0AFFCFCFCFFD0AFF27F8 -%27F87DFD5AFFA8F826F8207DFD09FFC9CFA7CFCFFD09FFF826F8F852FD5A -%FFA826F827F8A8FD07FFCFC9CFCFCFCECFCFCFFD07FF27F827F87DFD5AFF -%A8F820F8207DFD05FFA8CFA7CFA7CFA7CFA7CFA7CFA8FD05FFF820F8F852 -%FD5AFFA826F827F8A8FD04FFCEFD05CFC9FD05CFCECFCFFD04FF27F827F8 -%7DFD5AFF7DF820F8207DFFA8CFC9CFA7CFC9CFC9CFA7CFA7CFC9CFA7CFC9 -%CFCFFF2020F8F852FD58FF7C4BF8272026F8A7CFCFCECFCFCFCECFCFCFC9 -%CFCFCFCECFCFCFCECFCECFA827F827F827277CA7FD52FF7D51F8F8F820F8 -%26F8F875CEA7CFA7CFA7CFA7CFA7CFA7CFA7CFA7CFA7CFA7CFA7A7F820F8 -%20F820F8F8277DA8FD4DFF7D2720F827F827F8272027F8A0C8C9C9CFCFCF -%CEFD0DCFC9C9A627F827F827F827F826F82752FD4AFF7627F820F820F826 -%2651F826F8207BC9A6C8A6CFCECFA7CFC9CFA7CFC9CFA7CFC9C9A6C9A6A6 -%F826F8F82651F8F8F826F8F8F852A8FD45FF7D2020F827F82626517BC97C -%27F827F8A0C8C9A6C9A6CECFCFCECFCFCFCECFCFCFA7C9A6C9A6C9A627F8 -%27F876C8A65127F827F826F82752FD42FF7D27F8F8F820F82751A6A6C8A0 -%A0F820F8207BC9A0C9A6C8A0C9A7CFA7CFA7CFA7CFA6C8A0C9A6C9A6C9F8 -%20F8F851C8A0C8A07C2620F820F8F8F87DFD3FFF5226F827F827267CA0C9 -%C8C8A6CE7C27F827F8A0C8C9C8C9A6C9C8C9A7CFCFCFC9CEC8C9A6C9C8C9 -%A6CEA627F827F875C8C9A6C8C8C97C51F8272026F852A8FD3BFFA826F8F8 -%26F82651C8A6C8A0C8A6A04B27F820F82075CEA6C9A6C9A6C9A6C8A6CEA7 -%C9A6C9A6C9A6C9A6C9C8A6F820F826205175C8A6C8A0C8A67C26F8F820F8 -%267DFD39FF7DF8262020F851A0C9A0C9C8C87551F820F8272027F82775C9 -%C8C9C8C9A6C9C8C9A6C9C8C9A6C9C8C9A6CEC97C26272027F827F826267C -%A0C9A0C9C8C85127F827F82051FD36FFA851F820F8F8207CA0C8A0C8A07C -%26F8F826F8F8F826F820F8F8207CA6C9A6C9A0C9A6C9A0C9A6C9A0C9A6C9 -%7B4BF8F8F826F820F820F8F8F82751A6A0C8A0C87B4BF820F8F820A8FD33 -%FFA827F827F82751C8C8C9C8C97C51F826F827F8274B4BF827F8272026F8 -%51A0CEC8C9C8C9A6C9C8C9A6CEC9C95127F827F827F82726512627F827F8 -%2026A0C8C9A0C9C87CF8272020F8A8FD31FF7D20F827F82675C8A0C8A6A6 -%4B20F826F8F82051FD047520F8F826F820F82651A6C8C9A6C9A6C9A6C9A6 -%7CF8F8F826F820F8274A7575754B27F826F8F8F84B7BC8A0C8C8A026F8F8 -%20F87DFD2FFF7D26F827F84BA0C9A0C9C87C2020F826F8274B7575767575 -%75764B27F8272027F820267CA6CEC8C9C8CE7C27F8272027F827F84B759A -%7575759A75752627F827F82751C8A6C8C8C84B26F826F87DFD2DFF7DF8F8 -%26F84BA0C8A0C8A051F8F8F820204B6F7575756F7575756F754B4BF820F8 -%20F820F82775C9C8A026F8F820F826F8F8207575756F7575756F7575754A -%27F820F8F826A0A0C8A0C84BF8F820F852FD2BFF7D20F827F851A6C9A6C9 -%A04BF827F8274B75759A7575759A7575759A759A75752627F8272027F827 -%5176F8272027F827F8274B9A7575759A7575759A757575A0755120272026 -%20A0C8C8A6C97527F827F852FD29FF7D20F820F875A6C8A0C97520F826F8 -%264B99FD10752620F826F820F820F820F826F820F8274BFD11752626F820 -%F875A6C8A0C97520F826F876FD27FFA826F827F875A6C9A6C85127F827F8 -%4B757575767575757675757576759975754B27F8272027F8272027F82720 -%27F8272027F827264B4BFD047576757575767575757675754B27F826F851 -%A6C8A0C97526F827F87DFD25FFA827F820F851A6C8A0C84BF8F820F84BFD -%04756F7575756FFD05752626F820F820F826F820F826F820F826F820F826 -%F820F826F8F8F84B4BFD05756F7575756F7575754B27F820F84BA0C8A0C8 -%75F8F820F87DFD24FF4BF827F851C8C9A6C95120F8272075759A7575759A -%7575759A759A4B4B2027F8272027F8272027F8272027F827F827F8272027 -%F8272027F827F8272675759A759A7575759A757575A07527F827F84BA0C9 -%A6C97527F826F8FD23FF52F827F84BA0C8A0C84BF8F8F820FD0D752620F8 -%26F820F826F820F820F8F8F820F820F826FD05F820F826F820F826F820F8 -%274BFD0C7527F826F827A0C8A0C84BF8F8F826FD21FF7DF827F827A0C8A0 -%C95120F82726757576757575767575759A754BF827F827F8272027F820F8 -%272752527D7DA8A8A87DA87D7D524B2020F826F827F8272027F827267575 -%9A757575767575759A754BF827F84BA0C9A6C8272620F852FD1FFFA8F820 -%F8207BC8A0C851F8F820F87575756F7575756F7575754A26F820F826F820 -%F8F8F827517DA8FD0DFFAFFF7D522020F820F826F820F820F84B6F757575 -%6F7575756F757527F826F851A0C8A0A62020F8F87DFD1EFF2726202075C9 -%A6C97B27F827F8274B9A759A7575759A75752627F827F8272027F82752A8 -%FD16FFA8762020F827F8272027F827759A759A7575759A757520272027F8 -%76C8C8C8A0F827F827A8FD1CFF76F8F8F827C8A0C8A026F826F820F82626 -%FD077520F8F826F826F8F8F852A8FD1BFF5227F820F826F820F8264BFD07 -%75F820F826F820F87CA6A6A675F820F852FD1BFFA827F82620A6C8C8A64B -%F827F8272027F827267575767575F8272027F827F82051FD1FFFA87DF820 -%F8272027F8274B7575767575F8272027F8272026F8A6C8C8A651F827F8A8 -%FD1AFF52F826F875A0C8A651F826F820F826F820F8202675754BF820F826 -%F820F8277DFD22FFA827F8F826F820F82026757551F8F8F826F820F826F8 -%F826C8A0C87B26F8F820FD19FFA8F827F84BA6C9C8A0F8272027F8272027 -%F82720272651F827F8272026F876FD26FF7D27F827F82720272675F827F8 -%272027F8272027F82775C9A0C95120F8207DFD18FF27F8F8267BC8A0C826 -%F8F826F820F826F820F826F820F826F820F820F87DFD28FFA827F826F826 -%F820F826F820F826F820F826F820F827A0C8A0C820F8F827A8FD16FF7D20 -%F8204BC9A6C951202027F8272027F8272027F8272027F8272020F8A8FD12 -%FFCAFD07FFCAFD0FFFA852F827F827F827F8272027F8272027F8272027F8 -%51C8C8C87CF827F87DFD16FF4BF826F8A0A0C8A026F826F820F826F820F8 -%26F820F826F820F8F8F8A8FD12FF6F68446868684468686876FD0FFFA851 -%F820F826F820F826F820F826F820F826F820F87CA6C8A027F820F8FD15FF -%A8F827F851C8C9C875F8272027F8272027F8272027F8272027F82620FD13 -%FF6FFD096844A1FD11FF51F8272027F8272027F8272027F8272027F82727 -%C9A6C97526F8207DFD14FF51F8F826A0C8A0A0F820F826F820F826F820F8 -%26F820F826F8F8F8A8FD11FFCA68684468446844684468446876FD12FF27 -%F827F820F826F820F826F820F826F820F82075C8A0C826F8F827FD13FFA8 -%27F8204BC9A6C951202027F8272027F8272027F8272027F827F8A8FD11FF -%CAFD0C6844A1FD12FFA827F8272027F8272027F8272027F8272027F84BA6 -%C8C87CF827F8A8FD12FF7DF826F87CA0C8A026F826F820F826F820F826F8 -%20F826F820F87DFD11FFA144684468446844684468446844689AFD13FF7D -%F8F826F820F826F820F826F820F826F820F87CA6C89F27F8F827FD12FF27 -%27F84BA6C9C876F8272027F8272027F8272027F8272027F852FD11FFA144 -%FD0D6844A1FD14FF7C202027F8272027F8272027F8272027F8274BC9A6C9 -%5120F827A8FD10FFA8F8F8F851C8A0C826F8F826F820F826F820F826F820 -%F826F826A8FD10FF9A446844684468446844684468446844689AFD15FF27 -%20F820F826F820F826F820F826F820F8267BC8A0A0F820F87DFD10FF7DF8 -%26F8A6C8C97527F827F8272027F8272027F8272027F8207DFD10FF9344FD -%0F6844A1FD15FFA8F8272027F8272027F8272027F8272027F875C8C8A04B -%F82027FD10FF2620F84BA0C8A651F826F820F826F820F826F820F826F8F8 -%27FD10FF6F44684468446844684468446844684468446876FD16FF52F826 -%F820F826F820F826F820F826F8F820C8A0C84BF8F820A8FD0EFFA827F820 -%51C9A6C826272027F8272027F8272027F8272027F8A8FD10FF6FFD116844 -%A1FD16FFA827F827F8272027F8272027F8272027F8277CC9C8A0F827F8A1 -%FD0EFF7DF826F8A0A0C875F8F826F820F826F820F826F820F826F827FD11 -%FFA144684468446844684468446844684468446876FD17FF52F8F820F826 -%F820F826F820F826F820F875A6C8A027F8F851FD0EFF5220F827A6C8A675 -%F827F8272027F8272027F8272027F8207DFD11FFCAFD0868A1FD086844A1 -%FD17FFA8F8272027F8272027F8272027F827202626C9A6C84B26F827FD0D -%FFA827F8F826C8A0A62620F820F826F820F826F820F826F8F8F8FD13FF6F -%684468446868CAA86844684468446844689AFD18FF52F826F820F826F820 -%F826F820F826F8207BC8A675F820F8A8FD0DFF2027F87CC8C9A027F827F8 -%272027F8272027F8272027F87DFD13FFA1FD04686FFFFFCAFD086844A1FD -%18FF7D262027F8272027F8272027F8272027F8A0C8C97C27F8207DFD0CFF -%7D20F82075C8A67CF826F820F826F820F826F820F826F8F87DFD13FFA868 -%44686FFFFFFFA16844684468446844689AFD19FFF826F820F826F820F826 -%F820F826F8F84BC8A0A6F826F876FD0CFF7DF82720A6A6C951F8F8272027 -%F8272027F8272027F82627FD15FF6F6876FD04FFCA44FD076844A1FD19FF -%52F8272027F8272027F8272027F827F851A0C9A64BF82651FD0CFF52F8F8 -%27A0C8A051F820F826F820F826F820F826F820F852FD15FFA176FD05FFA1 -%68446844684468446876FD19FF7DF8F826F820F826F820F826F820F82020 -%A6A0C84BF8F827FD0CFF52F8274BC8A6C92727F8272027F8272027F82720 -%27F8207DFD1CFFCAFD086844A1FD19FFA8F8272027F8272027F8272027F8 -%27F827A0C9C875F82726FD0CFF27F8F851A6C8A027F820F826F820F826F8 -%20F826F820F8A8FD1CFFA168446844684468446876FD19FFA827F826F820 -%F826F820F826F820F826F8A0A0C875F8F827A8FD0BFF27F8F851C9A6A6F8 -%272027F8272027F8272027F827F827A8FD1CFFCAFD086844A1FD1AFF2720 -%F8272027F8272027F8272027F82775C9A6A0F826F8A8FD0AFFA8F820F875 -%A0C87B20F826F820F826F820F826F820F82020FD1DFFA168446844684468 -%44689AFD1AFF52F820F826F820F826F820F826F820F875A6C87526F8F87D -%FD0AFFA827F82675C9C8A0F8272027F8272027F8272027F827F852FD1DFF -%CAFD086844A1FD1AFF7620F8272027F8272027F8272027F82751C9A6A620 -%27F8A8FD0AFFA8F826F87CA6C875F8F826F820F826F820F826F820F82027 -%FD1DFFA16844684468446844689AFD1AFF7DF820F826F820F826F820F826 -%F820F851A6C89F27F8F87CFD0AFFA826F8277CC8C87CF827F8272027F827 -%2027F8272027F852FD1DFFCA44FD076844A1FD1AFF7D202027F8272027F8 -%272027F82720204BC9A6C82027F87DFD0AFFA8F820F8A0A6C85120F820F8 -%26F820F826F820F826F8F852FD1DFFA168446844684468446876FD1AFF7D -%F826F820F826F820F826F820F826F851A0C8A026F8F852FD0AFFA826F827 -%7CC9C87CF827F8272027F8272027F8272027F87DFD1DFFCAFD086844A1FD -%1AFF7D262027F8272027F8272027F82720204BC9A6C82027F87DFD0AFFA8 -%F820F87CA6C87520F820F826F820F826F820F826F8F827FD1DFFA1684468 -%44684468446876FD1AFF7DF826F820F826F820F826F820F826F851A0C8A0 -%26F8F876FD0AFFA827F82075C9C8A0F8272027F827F827F827F827F827F8 -%52FD1DFFCAFD086844A1FD1AFF7CF8F8272027F827F827F8272027F82051 -%C9A6A62026F8A7FD0AFFA8F820F875A0C87520F826F8F8F826F820F827F8 -%20F82026FD1DFFA16844684468446844689AFD1AFF52F820F826F827F826 -%F820F820F820F875A6C87B26F8F87DFD0AFFCF27F82051C9C8A0F827204B -%4B754B75759A7527F827F827FD1DFFCAFD086844A1FD1AFF4B20F827F84B -%757675754B754B27F82775C9A6A0F827F8A8FD0BFF2026F851A6C8A026F8 -%264B99FD0675F820F826F8A8FD1CFFA16844684468446844689AFD19FFA8 -%27F820F8264AFD07752020F87CA6C87520F820A8FD0BFF51F8274BC8A6C8 -%2626F8FD04757675757527F827F8207DFD1CFFCA44FD076844A1FD1AFFF8 -%272027F8FD04757675754B27F8277BC9C876F82720FD0CFF4BF8F827A0C8 -%A04BF8F8267575756F7575752620F820F87CFD1CFFA16844684468446844 -%6876FD19FF7DF8F826F8264B7575756F75754BF820F8A0A0C84BF8F827A8 -%FD0BFF7DF82726C8A6C95120F84B7575759A75757551F827F82727FD1CFF -%CAFD086844A1FD19FF7DF8272027269A7575759A759A4B27F84BA6C9A651 -%F8264BFD0CFF7DF8F8267BC8A67CF820F8FD07754B26F820F826A8FD1BFF -%A168446844684468446876FD19FF27F8F826F84BFD07754BF8F84BC8A0C8 -%2620F852FD0CFFA8F826F87CC8C87B27F8274B9A75757576757520272027 -%F87DFD1BFFCAFD086844A1FD18FFA827F827F8274B997576757575762626 -%F87CC8C8A027F8F87DFD0CFFA827F8F84BC8A0A62020F84BFD04756F7575 -%4BF826F8F827FD1BFFA16844684468446844689AFD18FF52F826F820F875 -%75756F7575754B26F8207BC8A67BF820F8A8FD0DFF4B20F851A6C9A04BF8 -%272075759A759A759A4B272027F827A8FD1AFFCAFD086844A1FD18FF2727 -%2027F84B7575759A759A754BF82720A6C8C95127F827A8FD0DFF7DF820F8 -%A67527F826F820F84B4BFD05752026F820F852FD1AFFA168446844684468 -%44689AFD17FF7DF8F826F8F84AFD05752626F826F820F87CA027F82027FD -%0EFF7D26F8272627F827F8272027F8274B757576754BF8272026F8FD1AFF -%CA44FD076844A1FD17FF4BF827F8272675759A75512027F827F8272026F8 -%4B2027F87DFD0FFFF820F8F8F826F8F8F826F820F820F84B6F994B20F826 -%F8F851FD19FFA168446844684468446876FD16FF7DF826F820F84B75754A -%27F8F8F826F8F8F826F820F826F820A8FD0EFF7D27F827F827F8202752F8 -%20F8272027F84B4BA04B272027F827A8FD18FFCAFD086844A1FD16FF2727 -%2027F82775752627F827F827F8202751F826F8272027F87DFD0CFFA827F8 -%F827F820F82652FFFFA827F8F826F820F8262675F826F826F851FD18FFA1 -%68446844684468446876FD15FF52F8F826F8202651F8F8F826F820F8277D -%FFA87DF8F8F827F820F8277DFD08FFA852F8262027F826F852A8FD05FF7D -%27F8272027F8272027F8272027F87DFD17FFCAFD086844A1FD14FFA826F8 -%27F8272027F8272027F820F87CA8FD05FF5220F8272027F82027A8FD05FF -%5220F820F826F8F826A8FD08FFA876F8F8F820F826F820F826F820F8A8FD -%16FFA16844684468446844689AFD14FF27F826F820F826F820F826F8F827 -%A8FD08FF7D27F820F826F8F8F852A8FFA12620F827F827F8277DFD0DFF52 -%20F8272027F8272027F82627FD16FFCAFD086844A1FD13FF52F8272027F8 -%272027F827F84B7DFD0CFFA72720F8272027F8277DF8F8F827F8F8F876A8 -%FD0FFF7D27F820F820F826F820F8F827FD15FFA16844684468446844689A -%FD12FF7DF820F826F820F827F8F8F87DFD10FF5220F826F820F8F8272027 -%F82627A8FD13FF7D20272027F8272027F82052FD14FFCA44686868446868 -%6844A1FD11FF7DF8272027F8272027F82652FD13FFA852F8272027F8F826 -%F8F852FD07FFA8CFCFCFA8FFCFCFA8FFCFCFA8FFCFCA2620F820F826F820 -%F8F852FD13FFA168446844684468444476FD10FF7DF820F826F820F826F8 -%207DFD07FFA8CFCFCFA8FFCFCFA8CFCFCFA8FFCF7DF826F820272027F8A8 -%FD06FFCFCFCECFCECFCECFCECFCECFCECFCECFC951F827F8272027F82720 -%F852FD13FFA1C3A1A1A1CAA1A1A1CAFD0FFF7DF826F8272027F827202626 -%FD07FFCFCFCECFCECFCECFCECFCECFCECFCFCFA727F827F8F826F8F87DFD -%06FFCFA7CFC9CFA7CFC9CFA7CFC9CFA7CFC9C92620F8F8264BF820F826F8 -%F827FD2AFF52F826F820F826204BF826F827A8FD05FFCFCFA7CFC9CFA7CF -%C9CFA7CFC9CFA7CFC9A6F826F82027F827F8A1FD06FFCECFCECFCFCFCECF -%CFCFCEFD04CFCEC851F827F84B754BF827F827202026A8FD26FFA852F826 -%F827202620754B26F82726FD06FFCFCFCFCECFCFCFCECFCFCFCECFCFCFC9 -%CEA627F82720F820F8F87DFD05FFA7CFA7CFA7CFA7CFA7CFA7CFA7CFC9CE -%A0C927F8F8202675754BF820F826F8F8F87DFD24FF7D27F826F820F820F8 -%75754BF820F827A8FD04FFCFA7CFA7CFA7CFA7CFA7CFA7CFA7CFC9C9A6A7 -%F820F82627F827F8A7FD04FFFD10CFA7C9C951F827F84B759A7551F82720 -%27F827F84B7DFD21FF5220F8272027F827267575A04B27F82726FD05FFFD -%10CFA6CEA627F82720F820F8207DFFFFFFCFCFA7CFC9CFA7CFC9CFA7CFC9 -%CFA7CFA7C8A6C94BF8F82626FD04754BF826F820F826F8F827A8FD1CFFA8 -%52F8F8F826F826F8F820FD04754BF820F827A8FFFFFFCACFA7CFC9CFA7CF -%C9CFA7CFC9CFA7CFA7C8A6A7F820F827272026F8A8FFFFFFCFCECFCECFCF -%CFCECFCFCFCECFCFCFC9C9C8C9C851F827F84B7575757675752027F827F8 -%27F820F8527DFD18FFA85227F826F8272027F8274B75757675994A27F826 -%20FFFFFFFD04CFCECFCFCFCECFCFCFCECFCFCFC9C9C8C9A027F827F8F826 -%F8F87DFFFFFFA7CFA7CFA7CFA7CFA7CFA7CFA7CFC9C9A0C9A6C926F8F8F8 -%207575756F7575752620F820F826F820F8F826527DFD12FFA85227F8F8F8 -%26F820F826F8264A7575756F75754BF826F827A8FFFFCFA7CFA7CFA7CFA7 -%CFA7CFA7CFA7CFC9C9A0C9A6A6F820F820272027F8A8FFFFFD10CFA6C9C8 -%C9C851F827F84B7575759A757575A04B27F8272027F8272026F82727527C -%A8A8FD09FF7D7D5227F820F827F8272027F827F84B75A07575759A759A4B -%27202620FFFFFFCEFD0FCFA6C9C8C9A027F827F8F827F8F87DFFCFCFA7CF -%C9CFA7CFC9CFA7CFC9CFA7CFA7C8A6C9A6C92620F8F826FD0A754BF826F8 -%20F826F820F826F8F8F827F8FD05272627FD05F820F826F820F826F8F820 -%FD0A754BF827F827A8FFC9CFA7CFC9CFA7CFC9CFA7CFC9CFA7CFA6C9A6C9 -%A6A6F826F82027F827F8A1FFCFCFCFCECFCFCFCECFCFCFCECFCFCFA7C9C8 -%C9A6C9C851F827F84B7576757575767575757675754A27F827F8272027F8 -%272026F827F820F826F826F827F827F8272027F8272027F8274B75757675 -%757576757575A04B26F82726FFFD04CFCECFCFCFCECFCFCFCECFCFCFA7C9 -%C8C9A6C9A627F82720F826F8207DFFA7CFA7CFA7CFA7CFA7CFA7CFA7CFA7 -%C9A0C9A6C9A6C927F8F82020FD05756F7575756F757575F826F820F826F8 -%20F826F820F826F820F826F820F826F820F826F820F826F8F8269975756F -%7575756FFD04754BF820F827A8CFA7CFC9CFA7CFA7CFA7CFA7CFA7CFA7C9 -%A0C9A6C9A6A7F820F82627F827F8A7FD10CFA6C9C8C9A6CEC951F8272027 -%20FD04759A7575759A759A26272027F8272027F8272027F8272027F82720 -%27F8272027F8272027F8272027F8277576759A7575759A759A754BF827F8 -%2727FD05CFCEFD0ACFCEA6C9C8C9A6CEA627F82720F820F82027C9C9CEA7 -%C9A6C9A7C9A6C9A7C9A7CEA6C9A6C9A6C9C8A02020F826F820F84BFD0875 -%6F26F826F820F826F820F826F820F826F820F826F820F826F820F826F820 -%F826F820F84BFD08754B26F826F820F82676CEC8C9A6C9A6C9A7C9A6C9A7 -%C9A7CEA6C9A6C9A6C9A67CF820F826272027F827267CA6CEC8C9A6C9A6C9 -%A6C9A6C9A6C9C8C9C8CEA051F8272027F827F827F84B7599757675757551 -%F827F8272027F8272027F8272027F8272027F8272027F8272027F8272027 -%F827202620FD04757675752627F826F8272027F8274BA6C8C9C8C9A6C9A6 -%C9A6C9A6C9A6C9C8C9C8CE7C4BF8272027FD04F820F826F8277BC9A6C9A0 -%C9A6C9A0C9A6C9A0C9C8A65126F820F826F8F82627F820F82626FD0575F8 -%26F820F826F820F826F820F826F820F826F820F826F820F826F820F826F8 -%20F826F8F84A7575756F51F8F8F820F827F820F820F8F8F851A0C9A6C9A0 -%C9A6C9A0C9A6C9A0C9C8A62720F820F826F8F8CA4B20F8272027F82751A7 -%C9CEC8C9A6C9C8C9A6CEC9A02627F827F827F82751C9A051F827F8272075 -%75A04B272027F8272027F8272027F8272027F8272027F8272027F8272027 -%F8272027F8272027F827759A4B4BF827F82726A0A67C2627F8272027F827 -%7CC9C8C9C8C9A6C9C8C9A6CEA676F8272027F827F8277DFFFF7D27F820F8 -%26F8F8F875A0CEA6C9A6C9C8C97C4BF8F8F826F820F8277BC8A0C8A675F8 -%F8F820F8274B26F826F820F826F820F826F820F826F820F826F820F826F8 -%20F826F820F826F820F826F820F84B2620F826F8204BC8A6C8C8A62020F8 -%26F820F8F826A0A6C9A6C9A6C9C8C97527F820F827F8F8F87DFD06FF7DF8 -%202027F827F82775C9C9C9C8C95126F8272027F820F827F84BA0C9A0C9C8 -%A64B27F826F827F827F8272027F8272027F8272027F8272027F8272027F8 -%272027F8272027F8272027F8272027F827F820F851A0C9A6C9C8A64B27F8 -%26F8262027F826F851A0CEC8C9C8A62720F8272027F82652FD09FFCA5120 -%F820F826F8F826A0A675F8F8F820F820F8275226F820F82051C8A0C8A0C8 -%7B51F8F8F826F820F826F820F826F820F826F820F826F820F826F820F826 -%F820F826F820F826F820F826F820F8F8267CA0C8A0C8A07CF8F8F820F852 -%27F8F820F826F82051C9A051F8F8F820F820F8277DFD0CFFA852F8262027 -%F827F827F8272027F826F87DFFFFA827F827F82026A0C8C9A6C9C8A02727 -%F827F8272027F8272027F8272027F8272027F8272027F8272027F8272027 -%F8272027F8272026F8277BC9C8C9A6C9A051F827F826F8A7FFFF7D27F827 -%2027F8272627F8272027F82027A1FD10FF7D20F8F826F826F820F826F8F8 -%27A8FD04FFA852F820F8F8F851A0C8A0C8A6C87551F8F8F826F820F826F8 -%20F826F820F826F820F826F820F826F820F826F820F826F8F8F8262675A0 -%C8A0C8A6C85126F826F8F827A8FD04FFA852F820F826F826F820F826F820 -%52FD14FF5227F826F8272026F84BA1FD08FFA72020F827F82751A6C8C9A6 -%C9A6A6514BF826F826F8272027F8272027F8272027F8272027F8272027F8 -%27F820F82726757BC9C8C8A0C9A67C2626F827F82676FD08FFA82720F827 -%F8272020F852A8FD16FFA852FD05F87DFD0CFF4BF8F820F8F820759FC8A6 -%C8A0C8A0A051512020FD05F826F820F826F820F826F820F820F8F8F82626 -%5175C8A6C8A0C8A6A67527F8F8F820F827A1FD0BFF7D27F820F8F827A8FD -%1AFFA8272752FD0FFFA84BF8262027F82751A0A6C9C8C8A6C9C8C87C7C51 -%5126272027F827F826F827F8272027275151A0A0C8C8C9A6C9C8C9A07626 -%27F827F820277DFD0FFF7DF8277DFD1EFFA8FD12FF7D26F8F820F8F8F84B -%51A0A0C8A0C8A0C8A6C8A0A6A0A07B7C757C757C75A07BA0A0C8A6C8A0C8 -%A0C8A6C8A07B2627F8F8F826F82652FD12FFA8A8FD34FF7D27F820F827F8 -%20F85151A0A0C9C8C9A6C9A6C8A6C9C8C8A6C9C8C8A6C9C8C8A6C9C8C9A6 -%C8A07C4B27F820F827F8F8207DA8FD4AFFA8A12720F820F820F8F8F82626 -%7575A0A0C8A0C8A0C8A6C8A6C8A6C8A0C8A0A67B7C51512026F8F8F826F8 -%F8F8277CFD50FFA87D2726F827F827F827F827F82727514B767576517C51 -%7651754B4B2627F820F827F827F820F82752A8FD55FF7D522726F8F8F826 -%F820F820FD0BF820F820F826F820F8F8F82752A7A8FD5AFFA8A1524BF827 -%F8F8F826F826F8272027F8272026F826F8F8F82727527DA8FD63FF7D7D52 -%522727F820F826F820F826262727767CA8A8FD32FFFF -%%EndData - -endstream endobj 70 0 obj <>stream -%AI12_CompressedDatax}g{2y+ fN4J) jn}#ٲ$Kd_Xh5}A?a9"IXϖ|l沐Q?[e*.f62a? N,N1itsL?u (| Ed(@e!D(hܣ6f? ƂjM%.,$,5zlFBx08|I GH8DIAPH4 j[қMn5_1%IҦ_?w2p8Ӏ8B%3fwaЀn{67 ߣT[0(MHuM 𧋇̬֋>3oGq?"H EAT aAՠ99D8hC|S0|v"AL1$h~ -ũx,H10T0R"ibHp4ă'sJ<4G d?G`r@Yr߯F!(_RZ4̘6[w[$1 j 9 *{ :Ma9ʭ 6H-_iS ح9k>}>waY֕'%maZSo}f׊?+#/J;tk 'Jܯ~@MAYKBy z -^f-,5-ζÁ4/0h'}0+"Hؐ޶p`a$A5 |RLHao'`S?XecL,L,j.@x.`>CXk1XͧFpNm4W@gK>Qp J}gg oiaMXCD-G ~ )P:oئWht`"_niQ6W54Y-IM#[E.Xפg!!+_uFX2g_3̧G0 -_D8FAt%&cXrx B+E$E(̽P\IԉŨ` ->!/a"#q -XH^@S@~no_[?  U;#a &a`(HZ'Q2 e*|hB0HK`X[ث,ðQ8#Eo|) W71?B$oKDgEy ]- aZCč5AE n Ah<~FBQXgHc^: 7E b z_4#b=9?3 Zr03TzD"$З-`{ZfD3̀Aތ鰭 G{3g=SN,5eHݢQ$c$iu+cX8$IJ\1cI\}j32b$(  Ĭ$)*xqHZX6Q 4d0 -~¸+brs=rR&')F:XEIx"?DH.ֹ $2ޅC`S4D ϣ`r U%* j5 z| f3-GɗsACvސkj? ֫Xd~k+ o^09 7f`NMVc |{0U>Ġ{mu, -]>ǎɼ ͟Ӭ3P#2ݒ!SýT 1}^{LZ?ENejOmec5zX>|Zmpx1cUؐ( -GXpK[p\+Nh΄^̗"t9Id:14ddc .r|u%i?>4\Jo/\1)0O4p8=wnW*ܫ_:X<AS;xթ; =OXC#a`AAMt`kaWUy.S{*߮M3ё<- --?u -4 Zd#s0E^80i0cpآ[Y4H9g,܀ ЂG`NЙѝHZ/ ,nT#:=\-y- AD4 L8e6f f: kTSpfb, 0 d'-E؍+f -(KU?PJ~#i[] sd-/5 Ɋ SNiIr)xL@ԩ 0=cQ!ґKUH:}d* ԃ4 8q0c:`e J O+˰c,.F1]v@Vcq33J^xZLnk.`j'e#q[ӱ^j&k6lH4RqKYRnO;>tᦾWA*qYb@#sZ֔DemEx**NÉ -. FІ+fD4Ka?O9[THlZrXGR53\ ihwOqoyol)-&z[zܴ#r!bK}&Qpܧ0Z,P 2&{ {w&MjeV\M'Ct=7N ~cUhxl,VMRN )q഼G^#KHTv8}U vE獁h"AAAMbp^ ?[mϴJڕ/ <J{5lU1~/Ou©NWQ&J,ʼF>bz|݂&p#ow5/.@4b-Μi1Y"UVsdkDu`Wp^+)dh6:窲{h g=S渧L/,Ie|u3]#$>EN,0qNTPcnm=M91w(~m_V -OK#YD#m޼ss+ľĽc`E}&\5F l%,J oFy;l` ) 8hh\ |]I%J$Gcjހ |%ぇɈąh䳥=Nb&D4 tkI%Ne>z4{*07kU:._B-zE)"5lIW Qs48xC(wG"S2bF2Բ Z"I&| ʁUonn`}m4-vpNf_|q fD %iy信!cu|mBwr,t_tg`WvhuFЃD|v`gW[svpsɮm#}ww;[{KT$䠔G7hD@U1_]@Ξ01߼|{BrU&M)S=9`~{pTB$Ψk@y"1Z 8U2O:-& ~Jtۊ'u_Xz}~J?Q}y~@ȼ$fyT`?5jFba&Pa"nz_\Eq-gC,Azl;՝Ox[+xmvyC@u;ZV,v] k0tgebOjF.ժPGٗe=|c-ئ$ˠ1\%l$9()X([\}妛i|( 1Vb$K9j c\mMع{GgA9po|lc'ʚ6zNнK#U)k$Z_"[aq>4cntlMd;Na tp~5h\ަrwsKxu ._٦d@:.Xr#rƟ*|?hK'ob g8SkxPn.J1R]`{ -jtuX^/vm Yv{N<@b8#~~eO4Z<ȗtf;v+i[#j:p=sX{)\׽JLfv٭58Y\zqGW&Wt5CqjZH&d.~[lOX&*r/7 ->;0j`?E_a4?ј?ԻHSNհh sٱY_*7=3TeHhn =W<& -wZjs}K:ӌ/:~_^Fo/$3:jD`E??nT$$>hnyiVkp-ۊ縘Kke5Yv;ҨVm+=NL25(१ XYO%Y/{.:׊kmcjs_7AQxZ QeM=%l/.h`w#lv^+t@?hd:;# -DA 3WX*}glx7t2IQ@BHb}B&{*@ZvTCs:/Vax P. z92}C)|9u`8QE X[V,q?77z -2CP7gZ=0l3|{WЋTjh79<׷oe3;6D#pg"AQK0Vi"%&YtH{d}ia=Dnǟo@vBe BsYG>ceX@~ʵ*r&Upw&104^ Gа3jxg~-5]R] /Prx-0-.*J:ᨮϮ6`S["ٺ|bwq!+mp#Wt^rds 0K:@%7CZ:w`=H=\W_;Q*;!1#΁Q2^xXLw9Gg({C0R7 f^ - wL?bDd`}Zl44?_bћ\97yC<|vwwsaYӊB ,資?Cf`ZgjL%[鍫u eZڕOxz6;x^hΏÑw1~&cJAW jWONp>ӥ:=ϣdГ\>Z%w|bD41  ~}dzk-lbm슽fp8[z!4 '4||`r0zNC-Ъ(ې}vmghؾ*y'>85hF"w ~* sBJ^&6"Rd#Io.y+I`'Q ++;<ޕ )A| -.n}Q,WB5ikkEߜ?t]۫Uaq.4<{î c?[\m+ڱq6IX`3\ $)G(@q_C$fi&a7rЃ|M#_2Ͻ*zw7JBY=")QÅAդ 4&#)oq~At3B,oOŞB񑿵~byU\$Z ]lGp޸%`.C$lXYKxSāy@0Uk{YxǯГ}+ØiL[:+uyTeG%ʛI@ͲǀX0u*Ob7;LkU;jcxdƸ)6z-$:zC[ mNhˈ=_ɲ䭍 llRwPc,&+uN~Ks@O9_e=/z4Ojܸ?]Z2D^ @&DlWĐLq25U`d鐭ؽtؖ;л՗~3x_]?ELzLײj)I2I -!ث!jou/G}FѫՅ(VAmcZa-00jȗal_LǀfKdlaZcLR FWp¬t[*3 ֎,\2L~ZiS[dgDFP"Vs5[l8 -sR-"9l@ކ9<PCCRb3"h'fu!(lW@2p`]$wӥ#NZs"󮯑*1DIXcrxբHx8b$lD_+f&**h2<×T2|N -vO#-SN;K9ڗ 0bDCIxs|Jέ#teeTkJ.T8bU@Fi1i^AL݃@b+?r=%QG?!~*la~Q~tFsB3Cc؆<ҍn1XTwiL"O;0^`.*ݒ2A[8h(1LAI'$B%*C(}`  3/%b8ݨ%<7 ( G j3dݐp? A+ѥZ\.E\cTҠ,%Wdxx, i3Цi#92AqxhhpGܲ\}@Z gr&VtT?pX$zh\@ ۚe d^|f9á.30;*^ vч"ZC*s=^sbQt:UtnC#W TYvQ'=,sђ0Qt=d7x Lv-,pH>s@ jH PWUif#hk՚;݀X`p8Պ4lzBMiJ&d +8mW~l G d=Y1cyB}s8gBy I/9-JuL9 aÙI5Gg7R67hGsIF - 1˭qCNS5˝p|CnDKzܗ\7||D""X<%D4F5G &Rvz8W ^.93 p|+JRLR~ -="c&/* 4WKx#ՍF*LPuJ?9lң*yi=4-v3l3~CvFb7 %#:shd 1ޗ$Fx=֑l'3ñ E VQɥVcl D&kMNg099DNqX4L#1 !̒@Wmslf|sha!Ã/CL%h[#e -#hgb2nrTkAK7iYcȉ4F5>in!c#Z4Yt$sS+e-B.B#׸J%RY{{QPGxI.~ٙhrwTa#÷HN֓ - |!HFuݣeA ,y hcՐ@;\ԡhRU!"mOl>7F، ]["`QӶgJ_f6]GH#0{`̺,ZvKL0K9ѲoU!e-t~ {#c_&}\>vHmWw064/ 2yf0yF1'B3!_HNn8xOӿ9xTLdVe =ER˦"LMp+<@[ҟO½e {f oT( -oUYVÚ\"+XXffI-nNYou{\ؚs=6.? :ROD1_{u_{ FqqTͮ67n2 D5*)`>!yR%8`+Qvo@ia^mZ̧+OBuvZ=os*KG f$ʢ3Fk6uZGib^IFoP"3ƚ<8쫵Vf۝{C -Ј"qcW_qmQld5z^\Ó/5%۪q[ Q| [Յs%E"/[x3RMmž>9 - -59 -kPc'0/X5 7G&֎5;DHn$Sku1g) &U֍#&uaEˑK D1NUv??5~>*ZXQQ1s3ic}^/㱠&nŷDXz.sQXuDa{[ӟB4HC.7AХ[FX)GYJ܉ Ur/uy8{ŔO/Z Oć1]?;S " u6}YPO_WOᠬ V,''x^}sO඾%OUG>qx*#Z}Zu^Sf1+:z @'ºOoPtԫD^V>z_2Z-usN-c쮄2QvN8O= 4O*4^ɝk0qoxSY[Q4qNa9bxω6tb7y+vC8g3n?hk9 -^܎^Cs*a(sN9|@^H-@~& M"U<9C~[`z=p=w^OX=Ij5b5w- EjbK7(~=u4pj7Iv C.:X7 ]Cˈ/2.V@kJ+dZÅEGkSMHn #B+t-&XV.Vd";E+4Ry<~ F n4mt -GE;>"kpǩ,G# s. j[c8=sYOZ|?RktM]ɵ҉`e7D*^z%/@AQGqZy}xWd\]&!R_5y$Uv'%Xwn@G)g .aٰR 9U hiw^qCT"O}w -«?3 (b}a5S4&Э;Bh7#4Ո^RPѰHwh |"y`*}K\p,c$H^T-~{ ZȥB Q 3R0R=>CIx*"mu -u-x|lVS<>B':jOaF\qoF%:k:{pE?/9P|ythM^CbK A*Mu :k뙶wM2mt .kyB6ss(zMfR  4f@+}H#p6mIzǻGبCrjY3ULV3!ԇ,tK70mIc`74luf0-JƋ"R,5MIE6pҜd B M -VRaU\k)bnmonuV>-}[O>9s(8C#wZ*=-nSX`]C=fbI.s2ɵD[K٭eT6em˹XiĊa}#37$U )J \DI>`"?$jDyZ-d3 `V3T}T@«T덂y, -V|!֝,|nJl>7-(]#8=> ȅV)n:L{~lVeY@#lcq+7uܲf5;уCMk6 -ܐ;J0fз 1XLicT(W[@ I,$!7;df݀pf֒ %/XYF{i 4sXKAsTT 2N4$| ԏE@U T6m\@ʧxf.rɸoJP;++\ToՕ[/:Bs(-蘞nq(U>7pA GPfP! =QԂ"P@Ppyd&>c:heoO^,d\~T'F)ԥ7FĘɦv\*y$nQE&cLM54Zͣ$xL50 -nZL 2n9h -76$4-?[iHmۅ#sG4r[XHشwqs@$bsbB(N`hcz#͇!A=i^9w -[_GMNY_rAH~h+fGs4 زc-k?8[x{?F8*\\U:N436S;t߅"} 47 -,9QhXnZipڱ K-0Ĩ[l-de*!CRˍ K5'f. 6'L0L~ z` - Z/ .۪\,]꓄-y{Ŋ \*|twޗN;W9q-J?E9q-B^g\ǗK/3;S.\g\МϸLQxp9q-Z|vZ>88[y`9"Zkl0h >J$H5ni89 XbIVQߺW@J!WƦs-H*E@JT"΄3SK2c81*PH~9 WfyI73\~P]7S@r0]&rc\U3÷e&q¶w+IJ1/Af1Fp`~B }dVh'\_Fc#[P.`A"0R0.3y֊P?f쎣(3Ie~αjJAň42b6&%J򨧆I=&G ,JtL;&nBh&{ SQs$L W"i RFlQwp\bsשL3c01;7D+\zdฬr9P Q6Q.t['X1.jXlgHi1+Q(0$SE JѼ<+}2-:fWN -^xi:;Q..d"F1ov:Oc\_E*D[G]^u nɎGC=v:g'+ gE'*9E,r"YXv"Y"YEU$wеy٫Q%o$ȣOC[a0ޣZ-Eyss<0s*Sa(Ta.ʓ%Y0,FvV_~S\(6V/;>l@hXQu&G9n9!{,~kE?tNwޣL z/n6q{Sl -֑+6nmt|3H6sl^P_\BT0]2sIʯb?^` OIJet(HT5~&֞q_`U^lʲ;x;pᣢVݑ~]B/bDv~wQߪ -\VH؏6*l|nc-PnJeLZ^nNXاEq{>-K\0OWا;C׽O68Ov>aS[WRاUv¾ir ->EaID-p>}9Aaw➶ONZاe1iͰn\>ez6;37>/:ua)n>OCU) @+,>EaVU)fj,E4><]اU']NTاUէu_ا@^r>># j}>*d}Z}LaVUKݍ -CRWaOf>vGػu攁U1"G`2Xi>&-~XwEKGr}FPdp?zSz ؤ¸Z'meS1LسO0blt]vpZd` OhO.GG_fzfMѽG]'2TJ?E0/ei~am;I^(֛p0wGț׮k 1QMuMgbYvQvZtīwJRH*,i*lyW4m*B5 IIаrR4$rvOSy>хt V3CDpwHڷxCML^Sؤ%'R60)u6 -vq՞? <41c,Gj\Ɔ͆rB̽J(G!x'8ƀdjs m -H.;HP̪()(Er"yԸzϽcӔ40?LS{DS}H? h_[T_? ?tjW)s#4Tվ? -[[Kwvt;ޟh@ q S@Wl^Y[KeVo4̄ܿ>1ԬVbckB*UN^53+#ՏAiF~dD=BOXhKf ߓ:Qb]ƸJxUTH%i -QGTFvȣݝ(( GwceϺ۶{/nmS4ډ.lPFn 1aXwJ05?9 var"8(Bۄ|\6pѷ޸^<>#ʑ:ϳ K&+ |I*64%.{okU東Q%̮ uE­W٣5קŕ׍}"XKV2s=+6%#*.]ΛNf"$K令8ʒCW5ZGr5Vz*ִ<^tL -͢|jWƕvgl颬i0G`$zD'8 ͗_C<aGc1Wgtܐt I3όu6RS84{$n)8w&ڋU_90]m٨{$n)H%. &&nXg UZ9`e }rnj7ZI=feP8uG:t5喫ibT0M.3 8{mIlNO;xZߝ>;9 $t3vm(::㤢hԢNrutx` Yk_{ E=}K_B?YQko0bd!K4gX3*aTUS%z= T쌎{1e/x*~R[[x{%_&E_ױ3T$ }0Gʹh/I4;f5O4UǻEzshI宗 {`I~XI"n=f$QTZV%f1pVZW[N٫$sϒDM.ѨG4J*ILQKB9 - ڤxl - 8eh4'B6ﻬPI -qvG^V( -e^V2pI1Qw0N:CSP(ӼPr3r.<9P fZ_xh\xvSЅ'< C"]x׺ {ٞeDjDPgpmqP%!!&ν.<4 J - <Cz\xHbGЇ^xh|ۡǝ_zۡr慇x]o|ۡ[*(4׾ѝi1Vnu yql .&Kv:.MԑFՏиT_qa0A{ᡢM)/<4ssԅ޽Tf1[LYdvmԂ8$Du"i^^r:,_Nz\֔FPo`9U }6_kH?ou7-,EM}-h/8 85){^"zc'o]s=)Gy?xAy oE"o|LHзz\\s^e5){v\~+|zo tD?#SP((_F:y -eZXŸa Nĥea8bI\RbF+J>y=KrwjvZ?G&Y}o#YS+hE lN)Jd7Aô2Y޼ !QNX" ,.wҟ!; )Qh85]GNU'x -b rlx?1r:ܔh NCc+"_pw6"@_`O -yv~,>{e"(G 8YT=s#,=pe, FX*v#Ik?H "<+KPY" 0[=S9T5~xD)'|.t7X|{k.^ -w, ZS(1l8w -B+*U{u.9yԱ,SY'џrZ|,0`wŠeÚϜ¡ǚ҉=ΜBiJ2Ln9ew<ąS@Z9]ZD}ugKYB8`2 z? -?"e#D_M3}d/ި) -Ɋ.j6RGJk$W~sT'8=ި&9I9/3@P/X$\AlS{շ3_yst|< -ԃ N=huChzh a;]ݸ}O44F.Nm]tj*0|бfzL={ -q{mv71/]ڗ8hXš%vj>\i:{=<,0es*='Ƚπ{j207g3jF8Plk^b+s&ݼcr_tcY\rt,l1"sF>8`lv8CNG2M=BA9;PXP&gp{%)|~oM|>3H$ >lb;ޑk^M7jL9ߊ/u QL\m!N,*PehN8>|Xw@OaC:ǣc=5x,>4`xW}-G2bw6w37ƢSzը2?ȱu8sS4r+[L\qartnƓ -F]B(dc@.Pw/ntAlrE.8[Kl@,JPL^N4iY`Yiz) g4om&0}\zd7/w2B4ŭ7ɫE W wgWtQz0ҮwMy WwWS}s ?7+Wv(=L<=jrDkYW[d|M{쟇J!*\ tH@:I#%,扐z.OP@Ly1bF]"$fK D"a$д&U$5y+3$g]w3?M~.3G9xZ&H2Oq`Y:JN/$BشІ==f $5H5/7*D":iBl ^ -97rF!礗XߺT67w!["P0k -C 5OkJ"Iatج㏑XDRB->{dR9ÈLGҮqqߤVϕqN8 6_K YUiqHQ嶴Nhr @==8PnN+Q)Gn'$f$e & LiXaZF=N Q?<$OH$CߵlJ-J%6R@# &'b>0t[]T}mN"T3|uTbeV'W}H/Q= ny-UU܌j=_ds@wB.-) -'<|**BeKS?74[*:4pe Ү'{E ]NKmS?Juv.\Sib!f"b5WjG:lҵ!_jZ޺qQf -ZNkE_ʹ,CsC67uG -RX|j9<2tiJ@BhUdhAͯZ@Tb<=\hJ!H8aڻuPǒlfߗʾBiCBC=|wfd;^P}NlFO#Ghzn]n{pi|$K2Rzp]gz_~lFu6RĄvk웣ӥ<E)mglpGnY8ЗGb둱pywirvâ2=ƇmmiYSE\#ucCun.ܟ)6GyV[+3po'W޵&[/lLYdu&(kP&PJkP&Pʹ (M.;l/oD[ c-uq+22 ]z>?hCK/(cnv L՝ł63nt_~NHboXIoD~oaO홟fŰrNH|i-#~BV&q@dž/2 Wqt:Y̕NRj>9AG:~ Ok* ?V)^AQ(2߃f(E7}nU˵};'6>2'Wd0EC|PB)H r,|ٸDK ݯFg(}  -;%xJyD0\tW }75QPNZƦNZzH'- Zt"vUJ~6r:ڜ 64d՗~h.v6Z,xU9书?5y6z׿4~qx9 GR,Wl.h_G, -H%N؜|_4x4,w6 -1eDޅYxod&I4%%%^ o[NmWvǎEՇꝂ5+s* UcxtJ:|rz{xKd2HNoVUU6䘷,*"|t=bbӫo_:\9i zYyGMݐnxnzfn]<4Z7XnMN7@yȹݹ,]lo3~o"8%5#|aZ&sycxJZǒJb8~c+y,Oi##($w+kʏn>ꋡؠ|>51 J_ߪ$G*~: -0&?ܽA*_;IYp d*]Xs'[3JN½|.u;b;NRikf@æv*<_&Msz(yS2cd -=zckBsԚl ?0p0aN$=\*RFe$c -Z[p|OCL}E[;-X'sJmo0i_j;,DV:JgͅyRjctp}mouAT3 d ã'o,scxѥ>o|wx m]rlho6bBR‚]87&nRf!]Z\w-u]ZP0KM1`e@ᚮ,8NR}㘖MnC;$6 Ӂp8ABEGP6nY7\v(@̅ iEG -h(QB Q:4tigoZq\jåbZ+mכ%$]U˞)28W)T|t~NheR)A2P2' Kê,O:%#@+bB62z+PڶǸ7ڳY-3mRvKWʥG|BCor8:R3$k؝$f=i D+ I :La$!X.K -H Zd. J"ni Qlaa40,Nq#Yrt'jo @ ~P+ma]T0i FW#3$X8g$Nt`ʰ`cX2I2iąIPdH Mڶm0Ri al!q -BbI%TX- "]T@z$6O;ˆ 8( 7Kp+oZ"}P 0ytV -D -,&IPdH@Ƙ ׎cpWK#9qk)@$YB a:_ -h"|E'Hqxx>΄&L B:( XÁq-Xf#~c:$=d&A#q2[跮EK#aU>JPx0SO,0/F4bpan6'x$]ۉs2A$F^[l x !<&ᶅuѭ$)0Lp@tX'@<QJ HL -8m^;#€c'u83ԓ K8!L "= -úE'HW*- mgH &U_ {# )F=HHBq@{!8dcNF$(.Af[S@ m )<'APqBcFz#d#]`SF2m1!\Cܙpt-kUZAhE_nZl900VzW6K`*^Z6^X-6hߵ4a_&ɫo[}?'~RivSq5hP]*Q * ݬcD%bD%|)ޏmm|eݥ(;-&U.5JTWS(jϛSmChKj5Yqyv隭kk;``:;emvʗtŇ8w6FVpkAUgHV=5T:8lPt W"';&FjkT~CuD5D\|^Siu`,S^o#STZ8cTڟvPjPV=\XJT"FTzVhk5lU~1s2~:^`cv,shqS:cP$D5D\|v?NS%*#*ª{U~ !{)H!=ܫ\vveaxQ _gOjS﹥Cןros6ls۾3x;{fԦws}sd#?0>c?-lyf84%dgL=JslºjPiuJ__TT".>w7TJĈJ :)syس -eS93Y*}%8[77OgNp:23}t&C熯7}8ma3iw(|Q3 -_ή$>5jUċӃEzJG_Ǩ[mJTJŇcD%bD׸zpZ'Ekt{G7t^p]ߚ^Jd~[߱xB1s2d*~ woZ d0ۅCi&&FYы ˬuz3F(V,'!K ev}߸BS,V\0txՏ._YNw5w34sk~I+ÓR>ۿz :.Wlqɧ?62qy\W?~sޭJ+;y%^#vCk+aԿm/7v5E#!:.I+9,蕝F4O{.:_t:в'YWGWir_yAysi~sfuв'=S_i?sJ3G[f} =(z#Y7hKmOz/vz(~]KƟ.3 Zo?I}E!ew6jDgR?C_߀^~?~dolT/hgTct/*MAotLfs?m~{^VK"Fk*&si[\oKz2Ppojꣽ? J}I2G -0Zm.M˰KؖL),̔:t3npv1ɵ;IFʴ sw).,CLpCбu?x)ڔ G3G9aSW'2]5wu6昘6 CsBT./uJ -uqG2Khh$RQO`T3S蘻0̎hkYzRi)+eҝ4dS5CqhBHmr0 b -;&@he/NJF܁h(V4D*ʡ(kL)-wC%8yWGI~dGEC%GWk]@w&lALXBk&'R ҟ1FK `ʠL%TGs[Bh `%2mh {v4 0&(p};5i0UԻHeT4YY\C*'2CtMb T' ! c]j+OL@Mik߀YT!Y^> E|ⷱۘ"\xLU9eSlިN쵦Ԑ#G | !Οmrdm~A~AG8Ia[ `8Z\*8&$L4v6e>t -(80 ,0vTgTC)7oY0Ax)b)mB7LCb5737q2&E11g.ga7/G -# yt9f -Q;IQ|Y <7އZJc6lF w`ε1]0zS})Lt  DUr :|%cu: fu.Cq Cf(ʎF?6ۗ4l C%u adCebya$;n\tɣk-%N$'@ -$DA|_RelZkK&}Mi#~X^a7 A}0喠5*So9T\k:zm&M0nQ)xWx_B9 -d0آ@bnއ}%"+ jiѝȆ0C#% 5p4P@mEB 2CFZ2! ."nzB7Kn;zuvпM -ϕ='q;UnWޚth򈰉02 ߤbY"u*ΆQ{7=笍Ț`>b&@ CUƴZ2[*r~=ïo:^Gˍm;M;̒,Y(. 5JܖDctFxR|ff -+p;C <%3?6D&%Y:@,qd7A-a)%f,lLoC='H@_!"ɀ@pڈKk.(ްw9WWkwRφ?)WHY4 1hΆh60IW&6X3Xr1nT7n6}?eʜ"麶4M f,f%J3&&75!Ƃ[8y]@;0 -UuɄ bXIR(6V}zvA2T+WwRy˷HjFYwRPappqc- -endstream endobj 71 0 obj <> endobj xref -0 72 -0000000004 65535 f -0000000016 00000 n -0000000076 00000 n -0000032391 00000 n -0000000005 00000 f -0000000007 00000 f -0000032442 00000 n -0000000008 00000 f -0000000009 00000 f -0000000010 00000 f -0000000011 00000 f -0000000012 00000 f -0000000013 00000 f -0000000014 00000 f -0000000015 00000 f -0000000016 00000 f -0000000017 00000 f -0000000018 00000 f -0000000019 00000 f -0000000020 00000 f -0000000021 00000 f -0000000022 00000 f -0000000023 00000 f -0000000024 00000 f -0000000025 00000 f -0000000026 00000 f -0000000027 00000 f -0000000028 00000 f -0000000029 00000 f -0000000030 00000 f -0000000031 00000 f -0000000032 00000 f -0000000033 00000 f -0000000034 00000 f -0000000035 00000 f -0000000036 00000 f -0000000037 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000035173 00000 n -0000032792 00000 n -0000032996 00000 n -0000039463 00000 n -0000034024 00000 n -0000033061 00000 n -0000033462 00000 n -0000033510 00000 n -0000039350 00000 n -0000035688 00000 n -0000035772 00000 n -0000036154 00000 n -0000039537 00000 n -0000039711 00000 n -0000040999 00000 n -0000061299 00000 n -0000092851 00000 n -trailer -<<3C78A8342581C9459377D8EF17C1EC5D>]>> -startxref -92990 -%%EOF diff --git a/data/tr1/logo-dark-theme.png b/data/tr1/logo-dark-theme.png deleted file mode 100644 index 6ddef13c2..000000000 Binary files a/data/tr1/logo-dark-theme.png and /dev/null differ diff --git a/data/tr1/logo-light-theme.png b/data/tr1/logo-light-theme.png deleted file mode 100755 index 9ba6c079c..000000000 Binary files a/data/tr1/logo-light-theme.png and /dev/null differ diff --git a/data/tr1/logo.ai b/data/tr1/logo.ai deleted file mode 100755 index 2cc0ab4a7..000000000 --- a/data/tr1/logo.ai +++ /dev/null @@ -1,1929 +0,0 @@ -%PDF-1.6 % -1 0 obj <>/OCGs[26 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - logo - - - 2023-09-25T22:02:55+02:00 - 2023-09-25T22:02:55+02:00 - 2023-09-25T22:02:55+02:00 - Adobe Illustrator 27.9 (Windows) - - - - 256 - 148 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAlAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9Qyyu7+nHhQ19Ubu2+Nr Tf1Q/wA34Y2tO+qH+b8MbWnfVD/N+GNrTvqh/m/DG1p31Q/zfhja076of5vwxtad9UP834Y2tO+q H+b8MbWnfVD/ADfhja076of5vwxtad9UP834Y2tO+qH+b8MbWmvqjdmxtad6dwm4NR7Gv4HFVyXO 9JBQ+ONKrgg7jAl2Kqck6Iabk+GKqX1iVvsL/HDSHf6Ufb7hirfp3P8AN+OKtcbod6/SMVd61wv2 lqPcYquF2vdSPljS239aj8DjSbbFzGSBvvgpVXFXYq7FXYqhrWnNgftYSgInAl2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxVZJErjfr2OKqMLtG/pt07YUInAlaY0Y1IqcVXYqomZhNwNAvj9GFCp6s f8w+/AlsOh6MD9OKtSOEQt93zxVRt4uVXcVr0woVfRi/lGBKnPAvCqChHX5YUL4JOaUP2h1wJVMV dirsVUJYW5c4+vcYULfWuB1X7wcVd68/8n4HFVSCVpOVQNqdMCVXFVCWd0fioBwoa9ef+T8DirvX n/k/A4q715/5PwOKtxTu78WAGKq+BKlPK0YFKb+OKqfrz/yfgcKG/Xn/AJPwOKtfWJh1Tb5HFVWK dXNKUbwwJVMVQ90v2X79MIQVdG5ID4jAlzkhCR1A2xVD8LiTqaD32woWSRemwBNQe+Kq31WPxP4Y 2tNpbqrBgTt2OC0qdwS0oQ7KO/z74UIlQFAA6DAl2KuxVCEiKY8dx3GFCKVlYVBqMCW8VdirsVdi rj0OKoe0/b+j+OEoCIwJQ8n+9K/RhQiMCXYq7FUPH/vS304UIjAlQu+i/ThCCrL9kfLAlzOi/aNM VUzcxDpU40tqUQMk3MCgG+FCKwJUbo/uwPE4QgqkQpEvywJXYq7FWmRGpyFadMVbxV2KqVxHySo6 r+rEK63k5JQ/aXFVXFVsjhELd+3zxVQgiDgs+9en9cKGgWgkod1OKooEEVHQ4EuxV2KuxVx6HFUP aft/R/HCUBEYEoeT/elfowoRGBLsVdiqHj/3pb6cKERgSoXfRfpwhBVeXGPl4DAlDxRmVi7nbChX EEQ/ZwJXgACg2GKuxVCyH1ZQq9B3/XhQihttgS7FXEgCp2GKqbXMQ71+WNKs+tr/ACnDSLbF0ncE Y0tqiyI32TX2wJUGhlWQmPoflhQ3S7/zpirRiuHID9PHbFUR8KL4KMCVLlHMGTuOhwqtt3KsY2+j 54oRGBLsVdirj0OKoe0/b+j+OEoCIwJQ8n+9K/RhQiMCXYq7FUPH/vS304UIjAlQu+i/ThCCqMpa EgdSMCVO1cUKHrWowlAV8CXYqh7iV6lAKDx8cKFSGIItepPfAlUxVZLMsY8WPQYqhXaRgGb7J6eG FCIjhh4hgOVe5xSqcE/lH3YFULkIqgAAE98IQVog5IGRqt3GKrknZTxk+/viqIBBFR0wJWyPwQtS tMVQ4WWY8mNF/wA+mFC/1IYhRPiPf/bxVq4WoWVfpxVWjfmgb78CV2KuxVx6HFUPaft/R/HCUBEY EoeT/elfowoRGBLsVdiqHj/3pb6cKERgSoXfRfpwhBVl+yPlgSozQ/tp17gYULoZ+fwt9r9eKVXA q2SNXWh69jiqhG7xSem3Q4UInAlTmRGoWNOwOKqKhwTCwqD09vfChXhjMa0Jr3wJbMsY6sMVaKxS eDfI4qovC8Z5RnYffhQ5pY5I/j2cdKYq1by0PA9D09sSqKwJUJEmdytaJ27DChoLbx/aPJsVXq6z KygUwJWWrEFkPzwlARGBLsVcemKoa1NGZT1P8MJQETgShmPK6FOxH4YUInAl2KuxVDKeN0a9z+vC hE4Eoe7IJVe+EIKIAoAMCXYqoTwV+NPtdxhQ3DPy+FvtfrxSrYFQ1yQZFA64QhE4Eqc/Ex0Y0qdj irkAijqxrTqcVUf3s7bbKPuwoVBapTcknG1pRlQJIFQknFVWGYk8H69jiqy4jCMGA+E9vfFVszxt x4ChGKomF+cYPfocCWp4y4FGoB1riqkI7dftPy+X9mFCpHLDyCoKV9sCVM/BdexP68KETgS7FXYq oTQnlzQ0PfthQp+rMfh5D8MVVoYeHxHdjgSq4q7FXYqpTwhxyBowxVREsw+Hl+o4UKkUB5c3NT1A 64qr4EuxV2KqM8IPxghT3r0wqo+tN9nl9O368UK0MBB5uant3xVWwJWPUyItKr1J+WKqV0xqq/Th CCqpwVQoI298CV+KqMcLCQu5qe2FWrpOjjr0OIQVz/vLevelfpGBKjG8QjIYfEa70woVLQ7MPpxK hVlVWjIY0HjgSoBLYdXJwoXo1sGHHr26/wAcVWXO0it/nscQqJwJdiqySVUG/XsMVUKSznfZfwwo Xm0WmzGuNrTULsj+k30YqiMCXYq7FUM5aWXgPsj/ADrhQv8AqqU6mvjja0pqzwPxbdcVRQIYVG4O BLsVaZgoqdgMVQ3xzv4KPwwoVPqsdOpr442mlkTNHL6bdDihE4ErGH7xTXrUUxVRuh8ansRhCCu+ qqejY2tK4FAB4YEtK6sSAakdcVU7kgRU8TiFLk2tt/A4VUYfRofU69sUL7T7TYlQrSceB5fZ70wJ UB9V9/xwoXottyBU7123OKrbvqv04hSiMCVCW4oeKbnxwoajtyTyk6+H9cbVEAACg6YEuxVDSf70 r9GFCJwJdirsVQ0JCTMrd9sKETgStdFdaH6DiqHVngfi26nChEl0Cc6/D44Eob4538EGFCJVVVaD pgS3iqGYh7leO4BG/wAsKETgSpzvx47VNdsVdNH6ibdRuMVU4Zgo4PtTocKER1wJQrgwyhh9k9v4 YUOYtPIANlGKqlwwWMKO+30DEJWGJFg5N9rt9OKF9qtEJ8T+rEqF0zqqbjkCaUwJUgbVuqlfvwoX pFDyDK1SN6VxVZcbyqvy/E4qrysVjYjrTAlStUHEv36YSgK+BLsVdiqHk/3pX6MKERgS7FXYqpTw 8/iX7Q/HCq2Gb9h+vY4oV8CVrorrQ/QcVQ3oSc+H7PWvbChFKqqtB0wJbxVDzTFj6ab16kYUKkMI QVO7HrgSqYqsWRGkK9x0xVfiqySFH3Ox8Riqj9WlH2W2+kYbQ2LViau33Y2tK6IqCijAlCy8hLWQ bdqeGFDcr+qyonTFUSqhVCjoMCVKaV0b7NU71woWg2z9RxP3YqqRwKjcga7bYEqQ+O5r2H8MKEQy 8lK+OBKGjkMLFHG2FCo10gHw7nGk263dnLljXp/HEoVsCUPJ/vSv0YUIjAl2KuxV2KqU0PP4l+1+ vCq2GY14P17E4oV8CXYq7FUPNMWPpx712JGFCpDCEFTux6nAlUxV2KqE0JrzTr1IGFDcVwDs+x8e 2NJWozPcEg/CP1DFC5ZWMxTbiK4pckrmYo3TemKFiExTFWOx74q6abn8CCvviqrDDwFT9o9cCVTF XYqpvbxt0HE+2KuoIYTvUj9eKrLVdi577DCUBXwJaZFb7Qriqz0YhuF/jiqnaft/R/HCUBEYEoeT /elfowoRGBLsVdirsVaZgoqdgMVQpBnkqBQeOFCLAoAPDAl2KtMOSkeOKoUcoJNxUHv7YUIpWDCo 3BwJbxV2KuxVTkgR9+h8Riql6M8Zqm/y/twoWKZkctxNT4g4q798ZOYUhvl/XFV4t5XNXNPxOKqo WKEVO1e/fAl31iH+b8DirvrEP834HFVQEEAjocVdiqGnYu4jXt+vChEKoVQo6DAlvFWmZVFWNBiq wzxU+1iqnaft/R/HCUBEYEoaUgXAJ6CmFCt68X82BK8EEVBqMVdirTMFFTsBiqGJed6DZBhQiURU Wg6YEt4qp+unPh+PauKqmKtOiutDiqGBeB6HdThQiVYMKjcHAlsioxVS9bg/CT6G9sVVQQRUGoxV 2KuxV2KuxVCsTNNQfZH6sKFb6tF4fjgtLTW0fE0FD2xtVls5BMbdR0wlCpPLwWg+0emBK23ioObd T0wlCtgS4kAEnoN8VQqK07lmNFGFCr9Wi9/vwWlfHEqV49+uKrsVWPCjtU9cVWm2i98NrSkpaGXi TVTihFYEoaQtLLwH2RhQiERUXiMCW8VUJpiT6abk7EjChr6qeHX4/Dtja03DMa8H69ATiqvgS06K 60bpiqHjLRS8D9k4UInAlZJEj/a7YqhnRonAVtzhQv8AWnX7S1+YxVsXfiv440tqkU3qV2pTAlq4 cqlB1bauIV1ugVOXdsSqrirsVQ9yvFhINif14QhqGIu3qPuO3viqJwJdirTiqMB1IxVQtXFCh61q MJQERgS7FXYq7FXYqhpiHmVV3pthQicCUNGRHOwbatRXChE4EqE0xJ4JuTsSMKF0MIQVO7H8MCVX FVKaEPuNm/Xiq2GYg8H2I6E4UK+BKGciS4Xj0FBX5b4UInAl2KqZhBlElfoxVUxV1AeoxVD2wIdq jbCUK0iB0Kn6MCVG3cqxjb6PnhKERgS7FUK1ZpqD7I/VhQigABQdMCXYq7FXYqoy24Y8lNDhtCz0 bj+f8TirfpXP8/4nFXelc/z/AInFXelc/wA/4nFWvRuDsW2+ZxVVigVN+reOBKpiqnLCsm/RvHFV L0J+nLb5nChVigVN+reOBKpirsVdiqnLCsm/RvHFVL6tL05CnzOG0K0UKxjxPc4Er8VUplmJBQ9O 2FVMXEi7Ov8ADGkLxdRnqCMaTa76xD/N+BwK76xD/N+BxVabqMdAThpbUWZpJOSrQ+2+KFT1Ln+X 8MVaZ7kggqaH2xVVgj4Jv9o9cCVTFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXGlN+nviqi31bvT6P7MKrD9V9/xxQ4fVff8AHFV6/Vu1Pp/txSrClNuntgV2KuxV2Kux V2KuxV//2Q== - - - - uuid:edd2effc-90f8-4921-9f87-5c443560c5f6 - xmp.did:0fb69b92-9296-f643-8766-fd53738d52f9 - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - uuid:87d7cc42-d24f-554d-97c7-b76979e61089 - xmp.did:68099ade-e32b-42d3-94ec-ef546c954f1c - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - - - - saved - xmp.iid:0fb69b92-9296-f643-8766-fd53738d52f9 - 2023-09-25T20:39:44+02:00 - Adobe Illustrator 27.9 (Windows) - / - - - - Document - Print - AIRobin - False - True - 1 - - 500.000000 - 280.000000 - Pixels - - - - Cyan - Magenta - Yellow - - - - - - Default Swatch Group - 0 - - - - White - RGB - PROCESS - 255 - 255 - 255 - - - Black - RGB - PROCESS - 35 - 31 - 32 - - - R=240 G=243 B=245 - PROCESS - 100.000000 - RGB - 240 - 243 - 245 - - - - - - Grays - 1 - - - - C=0 M=0 Y=0 K=100 - RGB - PROCESS - 35 - 31 - 32 - - - - - - - Adobe PDF library 17.00 - - - - - - - - - - - - - - - - - - - - - - - - - -endstream endobj 3 0 obj <> endobj 5 0 obj <>/ExtGState<>/Properties<>/XObject<>>>/Thumb 34 0 R/TrimBox[0.0 0.0 500.0 280.0]/Type/Page/PieceInfo<>>> endobj 28 0 obj <>stream -HtVˎ%5 W*qrfÀF,Ͱ9vv#PK8v8o?݇;m_7&q&c=^JnL7m+UbUmRҔ_8yv-}ڕjiRP"u y#C+I+ZjlF[kފN/0ohB \j7n)I܅j7A[XjQ:5](FXu$F> c.:W`{qVcE5<#R#[j|E:0 -GK^yhepǵZ :L($;וwcPF bMY$1M=lO øj`5,A{m=.)u|)8y)8Z?vWh yʼn/8$W sG;mX/H@v*!?I.Ae_D/= kTY:Wf$2ep=1:ưz|@-"C>Ճb"ܴX22q\3UẊ[~|NִĢ T<9tX|AcCcOߥAGҜvɯgLOO6\ -endstream endobj 29 0 obj <> endobj 34 0 obj <>stream -8;X^=5o.?\#Xn-2(L25&YEN$lU,*;!Uh1AS%$c3+>KWG@qFo`/G#c9OH)C+/G4\H3 -5*DXT[ccoHM;1+qUDUP-OnC@$LN9bA[,4W;?T8da$?F+FF,edHIh2KH/P@8Pmr%ds -[,-5?BCR$`1V5ph(sVt/UiKnqH^of[ -endstream endobj 9 0 obj <> endobj 11 0 obj <> endobj 12 0 obj <>stream -%!PS-Adobe-3.0 -%%Creator: Adobe Illustrator(R) 24.0 -%%AI8_CreatorVersion: 27.9.0 -%%For: (Marcin Kurczewski) () -%%Title: (logo.ai) -%%CreationDate: 9/25/2023 10:02 PM -%%Canvassize: 16383 -%%BoundingBox: 619 -327 1108 -47 -%%HiResBoundingBox: 619.99564826068 -326.542200908796 1107.63695635689 -47.6737990912034 -%%DocumentProcessColors: Cyan Magenta Yellow -%AI5_FileFormat 14.0 -%AI12_BuildNumber: 80 -%AI3_ColorUsage: Color -%AI7_ImageSettings: 0 -%%RGBProcessColor: 0.943904638290405 0.953717112541199 0.961130082607269 (R=240 G=243 B=245) -%%+ 0 0 0 ([Registration]) -%AI3_Cropmarks: 613.816302308787 -327.108 1113.81630230879 -47.1080000000002 -%AI3_TemplateBox: 298.5 -421.5 298.5 -421.5 -%AI3_TileBox: 455.111416826001 -472.745786865235 1272.55140973066 98.5742204589842 -%AI3_DocumentPreview: None -%AI5_ArtSize: 14400 14400 -%AI5_RulerUnits: 6 -%AI24_LargeCanvasScale: 1 -%AI9_ColorModel: 1 -%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 -%AI5_TargetResolution: 800 -%AI5_NumLayers: 1 -%AI17_Begin_Content_if_version_gt:24 4 -%AI10_OpenToVie: 107.557309503033 306.532287769785 1.93055555555556 0 8752.6724174167 7952.96411510791 2950 1902 18 1 0 68 181 0 0 0 1 1 0 1 1 0 0 -%AI17_Alternate_Content -%AI9_OpenToView: 107.557309503033 306.532287769785 1.93055555555556 2950 1902 18 1 0 68 181 0 0 0 1 1 0 1 1 0 0 -%AI17_End_Versioned_Content -%AI5_OpenViewLayers: 7 -%AI17_Begin_Content_if_version_gt:24 4 -%AI17_Alternate_Content -%AI17_End_Versioned_Content -%%PageOrigin:-8 -817 -%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 -%AI9_Flatten: 1 -%AI12_CMSettings: 00.MS -%%EndComments - -endstream endobj 13 0 obj <>stream -%AI24_ZStandard_Data(/X|oq +,Ldj-YwgQF\7NSWo: 3 -2ݖUzN8缯 JCQIf ]p@$0H\m!%QSX"Kh8Xޓ$E%wM) |q -ZѦGueHܬ -\.evV~SFDU7b K0}@$.a8"B`%M gKq ' ͘0zV -aD*(. sEMݻmW ! %:[|7s2Uͩ!gû}vd+fl^f: ppFvtMDXqHys)Ciϐ, _ʞ]*pH$ qT02evܽȊUf̙84g9Y!29ojәR[eZDê !mUU!VPӃ63S&e/viT%tT%ve䡕!?k)ь7FIܨXbqL"q\҆q+!pD$É=+JZ U=; =5[p uYLx !1WэPX<#H2?/' c a 0<*ː2kQM/;TVxӮuTFx6VFfXߵ.1TvpXCCa8Vm$0A Z\QCaWTֈ;ppqXq4 -H|app Z(yTPbKO:<,CLº^T;t7gr{Vn8 -E&faF@:j8 la5 !3p;Cc@VZ &1 b@a83,FpHrĈCh%R ܪ0I1yX%ˆR / "a8PYцCBa8a8󠣎;8@PEH(\|q,3!18ELD3*W%Hx" š0a8dCqȅy0JIL P$WP+QÑg -#a XDNafu.Jx 6eHStٍu9:[q?`9$dBVKı3 ЎUs%d9n 'n8UӆbCd8 MYGŤc OV M5oLPmƄYO=Vr]Rkʐ̹[!^d+=K{*5ˊtz),CN&ѽs3,CNq0qu1P a- XM""18qq4;+AU`2A -"v;5B')yd"I.nqpHXG,9栃~ Db"PP$ÁpxP`AdȔ !RFShwsR֝Xģ_p`߆# uqW|1(Ì3jq#a(a8AGv<|0@Cq,A8DHG@$ -1 JTh0xH D"P$Q8EHWCP$ -B1 Z cq,BX,PQ)LAQ,D"H #hp%(1 "@ 8"8D!8qȣt,q781Gw6gfӒr 4H8=;áNQp0 8C#a # ߶m8 %p袊&(uӪD`!U) Ixc4aH 0<sa8̱0P #a$ 000 0nذFiFgaeAc,'n8$qnV8a6l>ex4#|:31 T`xGc"a!ᠢ5j5).q;8_^Ύnna -KjgfecRT2ʨWVpZP :ctnljhp@b8X@(P@(H pH -, A.X@ 8Al XX B`*` - &t`bA&  P`$P.P P@,AEP  .D L` !,TAB$`8 J0B $ -D@ !  L@ $PPl`$$.X04P, hPH8ABp(LLP8 & .L@A(Xp -p - A,,„ :Ѐ,! -.P`*H$ dPA -(@aB8@AT@`@AQA"Pf"xP$\AEקNTL*1`a\\@aB,L@B(PA@„  00%\@ D&Rs"P_OHtXi2;BcLjcٖ"H - x0t <(Lxp * PA$6@Ё8P 2J4Xh  -8 $h!$d`\! (` :̝.X0T - @„H8߽l X~RYDjU4:{Y*ftDT`B % H xxc5bF`$`P0aB*Ё @pJ\% ːZDIi}ҹyv/C.8PrPp H x @ >A@ `a 4x&8@ \ !  A.B * 8@B -(p :P!QA}ĒWrc {4,9DT] ˇ *0T`8xH\p -G&`@ -GfC"HH8P -"@Rs2@'H!A &@(2A40T`8Xbp` )` -"X ,TXd (T xT`8L"T *\`hAB $X@TP@X$|F,@TӔI"+ӣAC.iGZ𬐝{8eiL"V/,YrwFU~nyX. --i;UPlsXE[w%v2xJ4⋵NF5"M6..Kͭx)#*Vxt+sBf+g4ҘHAʺyN"b+M|*IG*?W-ߕ~{?2y|/6DjFDNfwܼ3Zֆhxs,tkf#JMF冈_L\JNdJ꘦L:g{$]_Րe9iioߊս[n7^Wn#37w#Bx8wO'|Ky.OEs6GsَXi-'"֤;<MPN;fϥL* $XBu)cn!cM$A*c7 %ƟkJb"3fh.?;syb$}~TXIbd8v s)IN[]) !{G7iٔ"A*|"JF*4vudmY -)KcMڵwmYjG>$denL9ui-6`: ҟ_6R#S٘2&)O=g*YTDJVZE)Ԑ4Y̺Q!WuKz{"YNo5Eί+tNudu"<»BN~WuDjܕ%3 ѯXJrDtQzή$4q^~Fvr뤡)[IpZEhkie٣^ĭ+RZθ̽ZږIWCμ+AQڲ|JG}LX fUKUWŨİT!ٕd}OPP'}喪dIʎFs#՜b;OeW6ܢ&= -x%tӮ`Q+}j t螰J,fyb`0s!!Z{Wӕ -9g7e&m2J*gEX.gIBڣs c49TܙSȏJ'vR:W.Ug/'K"YvRVy%%Y=v+G:8%]4V Gzxfґ^5H b37kOJUYamD?b!bˎXMdZ#DN}tSԕ]MG-XG4WBuBBf3Ւuve4&$c13KaK˔deK"[eMKS|&J`9mkdK dhx1 TrZǖ>:aɊOk]ͲM*d9n*yՆ5])#VsoƖC"i_7:&iQ -fTtN3e2>D6nǶ2[5aez|O"yH%M2?Ry -HN_}lef)^fڷ%A"ўGFأ&dSrе$!ZMh$[H'ehstb!ˣ]EoNG)gj<%,n7+L3BmY׊^WeB+z^J@2xCly!<2)D#RtCO#,W#h'}Js=6e^ -خrf*u2j -ȕ e_#V}1+kݟC}bb1bɫwJX5[g)1SMU=߮՛,YUHRUՌ% Q< .R%4Hepgi8В>?_Jk$߻-jꡗ4rNTeJXDϒnW:V -ܔ<":o!K##ȏ:/tTVvh&q0d&֡{"ĬnyeW͋Kpp>SJ#4 ~Kٜn4xr)dA[q1oNL#+sDx4G)s#; gtԫ!`Iw])Y):Ubͭi;Ey94e_EElRR#tixM9#b gJY F~1#b{y!;rIEh!ӑZo)"=tG u+xrt'o_ɹz򦴕#J+xBGJ OQNIW|HVieL%xP@ -< <Ȁ„DpR1=SkQAe_ -U:jlLo3鐍>75NxM |Y"XBy%,g$OҧplĤ] #i+@⮰DPgDDp @$pP n: W<: 0vO<+2y8 3 )DUEU2_U2gCl<4+Tr+yh1"bѝӛ|]%Uv -.fgr¹fYkbk2|9<#o{.Hn/ģقSJ|rXhzyx_`JZzhACBBJϫB+2*q!e窱,UYf2Q TB]X4txdURdyd^23k3vhso#:":clHGGǒ\y♻bY{4,K4Su3Ű[NQoBbmi7$,Ӗ?×M"%/ =thSN٘5KYO9k#U0MfbEXף:MuoY( ] -۱)f5]&D1{MvtS'wtr,3 ϮdsYⱱRY%}<^%I^y^ЉlO /ݴ)#IjMsDKYi"MX Xgs<4%&$XG9IS^Ϧݹڪ|˕Yˬ,6o4g|QY9,HDS$,$yJ%fF:;P͘gΪlvÛRn<3nSgc#Jg"UZj9SD$i -MiuBRȲfNB5MDz~$ckt*JSL?b? Tj39_̱DX29*)x̷n-VDpʒgduۭdxXɝrYx YXs,^}"ڌ/cS'Cw7x}go/",|3hJW;N -ãezSfXEcNq_, >Nu,,9N#'Z֘vnԲM=26IQmj6b*=i+sNG^I2Vr"UlB׎ֽ\ Ufޑn⍉V>G.sGDYIehT3QwrHl*H/D}隶>tbjO9 Z4>eI:;*U]9 ԑ\:2kTbޙ12#Qoس]4j<H؜3Q>Xb$HSoegYa!"-mO3,*J۞ǹDֳyzǿ7+uķ!!Dyټli콌 9zO5ߋ{}h#UܜXFtOߙ\wK2rT7͓DOMΪ$XV*:*c'si+{E_cWjsyZư&SY/M*VoK|TV߆rߐvSc?/];r f:b#n!#1bjUg6o!өv%^V1f_9E6{yLČWF)nn6cfʙljct҆ds˚x.'7Z^Tŗvv㙧],/] dvdi|ZK=ٗ4hDl -gŌ#?*O !%w:x9Ϛ唥o3>eL3T z&S*?^]FGI%W4>uzԕ"Ĭ_TW)y">;{^p^D]7O~h3Ϟ7Dr~|cZTdHhHwCn{fdtHdWbF= #ލeϨXu(Tp… -ؠɀ ,`bA @[k -e4iBHy0+*9L- -aŪS?h/xF9CfoB U>Ecˋ>WNJ)+CE|Asy!*^BlL^VgDUW)GE.e)|/UNNO-$#rI)u]b)\f}Wŷ MM$3̈́gLRW} ү4Gs8Id=?2,e[եkXiwU_.NEIط%=j9c6~^blաntWfًfViE>+]1iNw+^ۧYIh3_o,98uާp>hje$6;Hן '<ӛAOWW(Pij躬U%h%婗"sU^%|$QQ1YMI'VPI%QVSD*Yԟ<1Ny6E%/'VVI &) Kɚ:mw$[dRN͔#M\2 WICe4<{Nig3^/^Q].DrxiO]7SC宪ʴ󊚭ݰ4vuh/Ԗ;3ˌGd2;a/=5uzu3z|d5ׅNdIl%=UfW$TfIW Ká{ywc/s;F[Wu{n|mʖD*1Uiut镯JXcgu|[ב{=u/wMVpjCk2}d*OW;^{ܣfgveku#ϽXo޺%[~V:WV(p&>V'XcEHw>{!U!{fEFם3%/#OVcXu;:a]aq̅Vǎ8Zҳ̨̐^f6H/Azoٍt2{e.N?~ѝ2faNe#YFeB2>W9©/*f~y1fʘ^$2o&ײL2*r7IL:SbNDD*vOQ_Mdt7&V_C8^B*9Uykdc|妲|7)+*l]vVŬlUd]e2acBb]^sZ1a_j -ns>WA( X0`  $P4@  -  6@L# \;?Ӌ7"±+=$eNZɹV~9e kJ묲veeB]zyelhvhDC^5o~c)_JW~##7dX$R+VaUt+~b+tdIYwFwe9uhL" -O'ģڼC,fYq-v<Տ$V94GSOg!!SE.&цMUIWr,W,BcRYyJe7b*JF8ڼ*?*t{mϙfhŐty>55|$Lf7"$V٩~䕶&2YInJϫ_,M3Y~BUt̺It=mx=Ļg'fuuedNIʻJFga%R~C'qM*o.L;3]Ig3nNo쌙>2I|"çy5W2VS$vvҥ6FWke9rI꣟(ONgeɹZaݜD -L^YJf!0ωxv3dezR|ĖLE-mhffR-hB3b3f?xAJ>8F8vVIxjoJ4t *sևcovH*i>S9G9h.Jo;yT 347Rf>[?ʶ擕hj6R;GGawkyZ witdWYNe4+{~^_L!grjav^慎<|4ÓQp||_V*sXdj*jNJK̺G,TGRoJu48HՎ~t$+TsdI;Q~jvD!w8eC,gևuTU9~? |2+,8h9#Bt̥֬*eXfw=H[C.Uo2{$hԺ,0[h?17{E4lwhF76NO,Y?^bE5yrXN;CxC;XyC2΋pyƺJk1{ˡҬ=ڊܾ-+ʷP]V6z?J5Q5yG{XʗA\>.5488Hw4&K:Cu;Yһ\3xUʹfeS󩧦2/Y~H5O]#qeSʼT?*iCr̪ә.;+v,XF&)ͫ%+gksmW0+RK1%Y5yYIWJ%5wAY9zZrnUM=3HMa֦Uإlʫ K-tNsexI+s(")̋d9|ֲJ{`1[I6-˘8/[t[%V!UvGssVZ.+[J @b!I(Gq1@, =Zf܉ţJ o֣!9߬nt ߬Pbq)&_*7+;Y7- -uP 8\e'i*-J՗pVlQ.ZAN8F5Cz -[~Z',%1u` Z]8~ ,{G gKp ~TVKU>f0B$pVK^[f.r"-P]ݑb#<.&Sع5kii8kqgY߬́|g Ql$~c &E>l򣐇F !קoCֲ";l^q~8)'yîp@A;{ (x#aݬ3*VY3]5`7lX$2uެ’&X/nRPTLN0NwcAk2kYCPGIVvO%ژ;j5 t,*GW buYGWՋgŭ5xG aoZ5oAڏ9߬Wx,Ki8(ɝ|5T/7#, gb䩹?eYжm"2ud_ч-¥χMYZ2VH pslMN,t#'@܅N@p֥+El\ `[,|M* ޟ JsGa'X"gA؉,ƈ#eot -,ϭ}s$%2΢?PB?Ddqj -Y<%~- DPir֖O8Pd!qۦ[_AS$g]̳Y@#\TEIYVY[fĩ ;7UOg,D&gL& S/L~Vt.Rx=0Gf%".vջ8Hk JS,gzBgl|qAb,Vw2BJU78(HثG1 FɱϬ-ȣ~؟EŖ/ޝY bQ(8tG8tϰ!_yXW|='A2JY9LȵkHxT%Vک#5GغQ":A4NhT'~s(j '̏$լKyjji`r=d p2S2ǓIߌbH~ - w\ӥꅔa([ ;뺒"b P"nKrDN! X3`Ce!<+[}#QM(zkp1m$qbh:DŽ) v< _(jԈ՗.hep:,ݏBtiiD7? ㋞GЯNghg K2K5n#$`<1!HU&F-zcztwig#]Snh*>hBd-S`S'w?Ŷ^1:DgF[ʊٜ> A)6E* #L@G -2)|,Ȗdrs4E'u,έM:,ŠT2/2JsHi,vq/b)ϥƬ7`ij6}pتqw}.0!MT ZEpo >9biZ<8HaTȹc[W'pҾރ?X%`S/ydKKJPBx|9IJr&L3إ(6X\ C*94h'2ӝB̷"-+}\xr5X]ЃVrXH}Za(u Ւf;m LY*γVߚlÄ_x䰽uۡO`&Kk2q#уkC0~=Bzc j>% -d4jfFp3N =m]#Y6 -6a&:@]?n,@/7JlG(O;5}ůq8BDݚ<%;٬=dQFM\ϧ8B!e<FHq:lcftpn^C\ [ -P KŤt ,YdY<c:#/ kpo26Dib S,%(CtI,A凖䋍/FADC~4$6SaE7N*ƲRn"_.,CT1D0?i -@p:2W)򔖛8gA kqPӞ2N"4#ًOjjY5u:BcoO#_cP S8t3M3ll:5W"2ˁwJ19ÍAݛɅň%ȶkȈ^#[XT)W#^G"H|jrFQڕHGlpK 0-&9jDhOr :Ujz|0 8!贸k'vJeuХ nP8<1|n=OX,ho<@}(xYƝl.Gz~w5 ~^!R8ZBt}pYbK;[a ^*Ḁb,tUȾc-RX]F}4.B캱\ϋXsG6;7lTkSv<+'8oaVɳ j#QW{ph].AJ/\4A*"46x0ç6nsɁ]*E!|\l4}bv s&*)ClJvN n:}SM=2Vbϯ2DjFt V55Zm -ff+˵qr ^QKvMox: cPwx >u|Sxjqtٔ |H5 aFKga+܃AZ =Z+)IW@#>CƝf,0nYDa=7j `B#fx|0Q5H/#-\$ω)Oi(N*%0h+ʫGXa~) -$*2!nf٦Int&F|caRH{ d~ΞO)qu|=km+sҨ<6Q\8{ kh<+϶Z{B~ <[0Af PY# )!*1zKvNC f*Ѭk\WD:4G }b\C{U8"Lٍv.E3JH^n6Mwoȼ|1@>i1ڢ&9+’.qVeDd͒ 1E `̈́緭"HL%Y> EAl.a#'^QaXMjxSObu^yndfp=/K倃AʚQ䠊\@_Bi+Ƚ] ,ZP3-x~ a4E2Y -{15R/QLr}wX;pGVCكIQqEL!<46G z2Q_:W+%6Y{T cwB3?}JgWl!Pґcޢshpc_zw6zn U[|bG$C@ e?B0ESG鶹P3q>KR =5ߥ)zr0S -I|)˘"(sI!_#iY֡cղA!Ӭg -6WSmOS0Y^fQ 2 -$d l*6DZaU[z ~MPn3"&7ɴ71P}2^F5,?z/抁XbiYUd>'!Qq>KlSӗ-E\O!c!,UVC e۽ -@{l&h(rWm Yjȸ^ *7p"rH|*s)c \MGqʺt!$±1ſw[xL*ڱ2tS7XiJ*yJL#E.c]lI5><0NGbi]r]e[A&<ɺ6&r31KY~l :T?gWImFL,S]npK{y-'~ e%͵r5 vLݞbR*ec3oF v.+"Up߷* 1b6}r~d"sVyuѠTQ:^.pO >\ެw (YUPTFՅXX׿ˆwQ֐X@>_Cl?LF2͂`%ѱU.j"kl;wubUd@ġi.DE! /jPRp%KK򾔘&V)J$/3>+oS +4DAdp4 ?xLosg@\^} kz2%f$k 3NΠHq_\K`x\ @D7<>( \KFwЄ[;ͱKM56('o-Tɯez{?v],Y:LVj֣(iCG=e; :"ロQO-\3X&\NyeZ S%9}3|w]Ph/.8(ҏ)-3r4[R1'>w;NFyԎݞ<|i"*ܐGE٘JPRdtk+)ql&Fq<8p -7P`hrv2H ˻6Nh!FiDᬁ"ke: Ivݫe{lsPJW*!"WgFߧ@h ?ތ:`[9q b)ù+WE[o2)&xdDFa.WrbP#2c# ָdz!Q-Ĩl鐎*#9w\֛S'\tnXu4߈_\SEaΓ$ONI/le'e=I -##"ys$XAw.eK:$&&#KqAgAohԁ￑!+|9񩞠"F@6d$o,]^nnݒ 籸p./#as)%] #e"aO^ ֊Œap|xVbVv5(_p߆>yP׾_õދ㋚JF^!h5au!*Ê^~+® BS/bYS8d<hTw~qpf\3`; 2M" [7wȩBcaYfMޤEa_^OT#ޣ] -BJ2u! y'ݤ=͉}1!+*Ģ\qG}msDQ նfwf 6@ ;:bc+4v{rx;DyҏN¹G5--|'zP<]m%[\8S[L19t9d"`vOQ=#$x -U ݖj:@sF#ٟ2.Y)O56>E/ -0EFoUQ[}SLB ~|GV[qA>T vl ,#QsElrr_fGgV#ZSp5 6Q0<dE5 0ZhG:#Üoшmn/7L}wCpN>jc!ktPxĥieN-64Hӷ7Ph|Y!k -=4De*cٳ(D7+"A#Pe}LQt4 0D(}I<>_r%v#/`D0~Ai9m֥Z R  *;Z6^z@>?9դC@Z~z?X+*gև(s߃tw*bX˭\ZIf *SCS4-TLBvaNmpvu BPDjoL^W9ՕVa{r9+f7]a0"Q۝뎦NdK5ÀP߱-bpTn-ݒX$HN/%YiB~ -;<^OuC9ؑBy~f kDc^d8n*XZߊV`,a ~f^8 lg qbY@_-Qu17-bq/[rP$0S\3VKpN9Ntye&oj s&A+MufnX%p;3ۨ-ځ;x7q{a/Vjhz*\)醈=J!P$pIDpdi0fN%`ڭf][XXܐ|9r -w:ɚ3{ {.^!z40KLH;/h[y # Wdtg@Wg+3s@:ҿDLөhOM 24f_ڭn(m#E]eaˑpHHy -Io=]pu&?<>@olA;_6heW ^ol>(\p)DC@n."w}>FFߎm?0@GWr-A G/T -*5gRKQ3bF#1j:K2zfwEETDsT(U&O6{6^LlW+*Scg[m`݇` @b[1FoԪʌ wEjzWAQK5gs+0/Vokt|_qW{zr8, #zr֙*cE0->jKzXSQTbaUs:y#$iP3Gν<-ƻt.Eț{k8kj#*2{~ݴ`;2.ʣ𷎰4D Y -(:˛`޻FqxWmzr:!K *NS[bJV"@(: d2߉e!JDxL:THeP24wnb$088= q:aTXQl,BQxr+;z0{<}GI7]1>r+-?jTYbT*l}hT9^ɂO?CS@L_~`{uѴԉDsas!O C'4HOcyela*#5wU0/FFYhfkqQV" Θ\/rypp+\]*)j͌T r@+qMrH>8^(Obz^u+, -аQ9.pd&t`DZA[}d]!!'10hUMQ}\pF=MdžhMQjK\\De.H >Mu۶x `|ٝ|Ri0MQ\(j)[X0'&Mtp~u`TVSn+qh@.8ຳet +9nZJc ndjXH􆀖rh2Ҭ@iצ[WqѽYHe -{2]瀷obbd%cIcA:%&+ɾV*&fM8_!ܨpB/Tre?YiFY&~8 -Fׁ\Ɖ+{h >$ cQ}q]|%֙w7%q_FOҶ&X_{jDc%{Pa'J>tn1`otV"C2У{6}Jqj=%(/G`ܓKOZ4Ź ,B-.%Kt$eZ8(N,.nÓ[ιl;qW1a3yDqR0- -A(`v b+(ĒF2"趪G*oJ!]ܳjg&_k ұpEp_\Sqa? BL$ĹFNe&%KcLgڤq ZT!?l޴g\0"AРivj)ͯuW4[;HRSܐș^U7@PX" ŏSQ((fzDD]Wi٦*6ā\ J [~qMJujUw2~M֐&kR)KpwSࢷ/-QUK"w/(BVh)+8XD`LoeNL"_⣽go0)$il 2ogi$Eu8S6V3|@ Qfi$8qby:P29='[$cl1tj93$4^֘N#<}Z:@inK6s0]`M@iC!f@fV=/E0[()Mkꞝnkk0y Za'-MQ`54w"57s\46u?Dcr$p-7{o='+н'V<4+>:f,2Q]eĶUέVuU_ɺ E^Z\VW`(~OPN( TuN s&tɀ! Koщj2hs׼riOK$Ra#1A#V2P{fDYJl^Ca - F7n0tD/쎭z_#JUЊWf T`Wu.l."LLi V׬}Y[8Ji<W==ԏY2䩄=RGIT5!5`:;N_H`JGSz;|Vik/qe/$$yr3_oʁRg4Y4o%l1&g\ƆR6&T4=@zbOʚɐDCLERǐ7 CȄ@M磢Y]6cR:+&$hx`j+%Ѳ;Qgȱ:9""dq Et07X°g~pЄ $ĕ&`ϙ2S[C_}V.'6XFbh񶺇 QWY|8:e&.҅gzMA]9U۽_9 kLocY9,3olM=u&F*j礶+Ք=)b 6JśE\5T=-wE2HOvAبG[םZX6}tԧz +BZFw9+H(1[\q+IqAP7J¶ƪMŽcBXSW❆X ɡ&FzmhoK#賓xiMf!3/SGAkq?z'yQ!w"pV<!@mnV'+?_?B,oo _)1Q4cʒ\4%7TUƃ6*[H~޽X=@IF-4Lө ͑_:|4G|"HEp:dLhh%7~%/aCAKԕ3S >3+_C~H(J/`ntH5\Pʯwh(]ՐuZI΢-)GHπVcxf~9rz~C >.\LUg(=,~9"7px$ѐm.tW!WCp)YL%^ٌ;jO ^@;vDPSժ [gmRMbs$#exxH1} V"Qk !zcB7D1|ggK!Fu p"^"p/ޒ\ H56Y6m#d(Pj]Sq{=\uvx"b5l9ԶO ޙ מ[{Qdr-I=cnm zb1X14]{Xpԣ&`P1( p_<ש?a=! wrGI(N'%.Vt^3/#FwԵo7i\L oZzNeSTdR0QF(ea#U ,@sY0 n?lir;VF&.w&,W;Vk+#~ -?g2y?tsFK|32!֯>W7+8+D8FB$'?OW<s`(`65F9Ո8,a7( @J݃#! j3*ڟ.b=w4Au~TN@gEdĜ'#QLU# ߏhc~0M?JmJ4(#"{J]#I6SfRJ/œh}" -ojtpWA7Q9(R"κK&|"4N984ΫTN[n3l)5HzvYfhcLHZ-$iC`)C Y̮rsg[SKOW8BE5 -P;:(hl4j:A --S!Uk c|F50㢮 9ꒈ2YHF%2ۏXm1Ka}^̳w j;4Uڤ}ż#Ϋ+ ˺n֦:VQVޒ/Ӣh lh2|^/s$ -qMB-Ai`#t>:n0`P$A<\F=v쐄-#Mݳz,z=6YԸ|wYEEx^P ^%˧2R ^&,ymџֱQ");؅$:`?ʅۚۈp($( GH~Tw!XrL.\!nGg*Z[yl9B + _1ac#HGFoq K)*b6o9P/AHЄ鍧H(S=vPh8 $($R 3~%?xhR4r?.%0EkI,ө,%'27MmⲞ?|Mh^%A~B+z*f[nLI[$X̛Q鳄]CbJ]41ۄ푊+uAN*ns:*j7{$UX+((@i*%o*ȉ@ hd),_c(^@qȠ IUs}aʸx^jfXpAj_xe.fW@s3) -g$N(zV߰c6" :eie"NR2iyhM@qTƍBф`h'!yT+|aA] m8H]+ -gn<~3K,&!w:c%o,ګaf R6 .\H[g)8Ĵ91d*V V哅$d$YRbH)nU0cr>Sƕ;4ƮS8ԳE4Yy`vh7BP9!@An#p[zfM?0JĂSJwϒN{-*DJy=k 'buq=dqy(%Q 6+D@hHn}q)DaBR$kʱ$*2pkg?LO*6Dl}b@_zn-uJMF;xG#l(?rUd䨸aTT/`iD lV+ Z_ĩ\l!N.x4 Fdv'e1}ba 's !?&K [y66TwJ[IM>3jMhWXT\Y "j_ jݰX=0Ql b -.>Fuw?8h  @ee4Cה3H-L->_I'`n zRo H.{oh}ڍbc¸f R 4M.#;Xo aj G| DnۍK/ Bb6(< -)^\ghޚn!!L P 5er?|BÕ(Qx(T,/Ujb>8ʣe%qXOzM!x3"t8Ly\}C9bPW?^Xڅ"0% ['/f3oى}S 6?v02<[-:R>Y ~rzە9)yRJR"j1gBOAXڊF}W-#oXc15ҏX7 -?'c?<],/<`J$oG,LpXܒ=صơ䬁eaX/+X!kB`Y}ET T||rw%*vA2u} v!UW -gYqkR ]NDKSn,6"-4wV,i9]Z T:N{ߏW\󉱼0MRҰ㧮4+e3ilsrBYo]vACӽ,gar8 AgKηR135?-DoVS,o;k:95.xުzrzNϯ|9Ϟ}ؓDɡ'~T+JyUA -H6O7h5H%;ZM~'mPYAUL2FW_B)"@_*Tp!r:&QFᗳ;%$Oz%p6{Xf$wk9]<o~:l3Dp .)r.ת^+a7s:8NP{#YFNi\=gR !ʹJ ˷>bmv\PgaS7ZkDS}RiՏq1G& T=N|=FAQa#{ -V7lY\o7G6JCVB5&U߀0Ȧ7ٔ3:-W -3z˩ -;SJGH2<-D 0<i=.oca%h -%[(CC)"zcDd/;hD -k>CYw"n.6i -M+Mirgj*~H[2DCb[]qs?! `h,'&tמk -̄65-Ԑx9F;SNdΎtS=z=)s&3:>>DzٞB`Ro,EQW*}Ҡ~-6K9:U^/nܨxD( :,DTR{1/*UU?lcPpD-/C(QVEz~R@9~fL,0 r=q-v!By}ʶ2~>gPSL]yMOs_uN yOķIj='m .e|Hyrj]j2aCeRbTRfAb0+jXkJHhdZGxY$@@);&qΞv#n\۱NmHx1W͍YI _VBOGV!ne!މUƆ~s&T?5Gx8V/cUs:RG#;c:@+ 1>7T+<%;ރr⾛VW ;C 9eGpxݷk^X7ºgۀxM?mi-+]`6[\˙i -c>bB4]bEWD&U'@0VagA09+JV'+aK麅;\QOdzla冁DJňs'@:rTpT=F75u2|2AUxĐRܦD+17Ǝk .91Ce3ΙA*͹)w\7O:*^vsVB. g[8H-m}緍 -&(Ͼ[aT`Q!]俿W.sݡ ޫH㿌uG]F]ww393KxjJ rJߩPk|?h[ǞP&7&y5I\E dVItWUFqVwʰV<*R̽&VW`Y3XDv˜;π 77]6Qu /pN5{됧HWZqK~Qg^bf7yMKfX ŏ{K)vNڮ^*p53vqv[~#0egnrHSG?yt'o Yh -9)i@2H'A)}@[$mLhA Ċ -8 >6(b[PzZ7qeV$xJ"K̦azȨx%п$6n93=ѽԫ|?t..!}uhp~Sfh*rZEV2)&U;L}8X@y~t^4!H0u -lWJ)Ԏsez"/2)E5yDw73.+52/PV^9xnk%dgM䝍hU}_]o p4YOdwF+ehvJr콤6?O ?A S$#.%-G_NR9֢VZߋxٵK"s9?%o߃B!̈́kK7.П wz֟^o/>roof?G}Ooa rp$|SRY㦿luވg7YDbm`4)5MΛ(wyVb?ߑiM* -Y?KU&B& R{sF6YcNpp9t]zؽ:"Fav7#=r :e zqo呟F#A]ex -0:@P̊&T ;K]z*?\]wE4'9K^ AJwefnƃh%Id -NHRJR(jt*ϨgT܋_4.JO0=]LЫ;j/zO¾?dWfBVhK]Z*;q'2ɕ2 K}ҐWveKJE.eaQy:<<7McCHEHoaMz7O&b.qy:}3|EȞd%=%W͕+4#G;x~8?x8gF&pcpcl*-J?BJTEV\3;|?¹C辄i;{K8GxҾC3l-!JJ<^# -KtkBǐ< ' aOC6>=D2|'"(kDӞ K -K"3*-A"_d6/hR_eEcJfpZsAeOM+4 qyZW{ݑ*ͨ&, -MJԡ:QOO0aY]عXTZJ9x&z֜l9wZXN甪FJiN|KWoR=ž3lj+2Uk|k՚W]uUZ"^+g!qfcUG^U\WqWk$^?r2ԮK%#]bqwE~p2{mUS_睅mb$T4TC [s}{1ÚD;~+K/)y"HᏤ2WI=.I&DIC"Av?ZOb~˓/OO|中>ñ8/rn)eZL' eU - u Cuq. ߫>qcl71"Ff]b9="Lijtni11 8?'ê4dA&|dgECyL"*H SA y71-8M^_"Isd麨#yp5y(CJ)Cmd\O1%-&aRtSGǕ#M59TO\_PqA$C|`F7N!QD#1?C<\ HGѦhfhȇF)O#MɳhCg̠NnVѕ=B䍃,HG_&DQR̄|(p.DVT>'waEhu&Zsh;IƧR Rlޙݘ5p2B"jFL*4 N;R/h^Mg(+̫97{k>17t]1̳$RI'(U#(MF&UlWkKYKi 1$VG.a1ĥD |fA~yK!B(M IyH'.-a(2ogwHiL G#݋bHњJqhcNqJh* ^}fJ 'r_ XYy3(FFVf BkDרUiVL5H^*RXi*fefdx͌҂F,Q? rňi5 -9g1$NT ՆK(2ʈ/cǫt}~d,-qj2f8RLh7tTF.NvAb޲0Ejj%4P9N)̰R6T" -Y%# .:u3mVw~Y\y [UQffj 3(aN,N}]̹ NPnܬX2- lp8naABD(AuBd恈EDj(De,IY&KQKs {E?D(C_EڅrC2-W!YMK㉤>z3FD|Wa=sy.X;,*[CީT,?Xփ}5.Ҿj98j g=hG׮nD-Rx{HW2q3-gR|4bqҥ>3$W?d+SP홙_tc=r׹Is(q!{*{֗sxc]ctUjd$ɬB ""GJ Xh$""¤~QZa-81H냸o!5MMOHes"EQ~5Ch%3!!_xF [bceyϒDV椾L*1>Ò0{AJxIѤ,&_;yFa,&jS?.YӸA~p~rKE+3*U(gFDJDR!kȉ)sacOgwޤ&UӞ*5o[%M>Prqt~ufE˭c#\T5-JiVfzXe=oIJV9eER -6TMF$= !ʷQU-e{*b`8^~e,c/eh)̓ʪG,PA 9J<$E %uTҚDz9:Jm#&[Î~[RIQio_!Qe.V''pJZ5"1OoOJ:rRHMTM<#i†-Ǹ!S4#k$8HI`ɪLkwyg3)Cc6M;S{iD>-ȍ>yQ65 c]"'; 3Hc]z3lts]wqV<P#ɹG҄RcFQ'~*% QZ8Yig:[_s:ujd&,$&ihnAC/Y1*/U.z ye0xiS}~B''=LOԊ1E11_Q܏2(:1N''M#e3WRt:dt\:Hi5H+m? -!kWT-"Gi+6szQJLFz|E9j\FgfV]czE;L*SPmSɓY9YgR]BjCroS峒Ͱ5jI&ZobtϹ!Q&Hyt-QzdEI㕌HU*WYƐ,:Xu4];'%fX]OR2rIq uA7҉OCz+65Ԡ Y/QMzW%fEӇ#ty~1JBJ?VIyqz -9޼Ч ])EF4DNhxך^gDCQSԂ.>._"mzT'ެE.sd,] :>~WnhgFFZyGN91\ M"G R ƹ.34(]"1t/*EKnLHXuLPJʴ`Z j6R$#]=Fj6sOt1#%8bf toIGygF!sVG7G|&9ňv)e/#PQBIΚIS5ͫEbl.9 U n.1&hfpU¹5p.[nLgͬfܼ6LSGyƈ`tݎ -3C[5%TPNd"-DMP9U˂e,|1VDCSlgMR- kIJJ rj=I ٕΕm) -z42ֈXCSFVriZ*aj{PVE,DQQ8)W&?:z6U'+= )Q5WhM(R;İRZ=d*)Qb5ۯO;4Bdb*RHTp1,!y8%_9CcdD}jTb#ʺ'GaTaq0#JoYY 22Xlc$vCycաWG?gw"e Wv+wsZ:fka +fKqxI֌Lh4j(e26cHh?arV?~#~ "jB#Yky4\})bj(+&JqFċ%DLTj&+3e -!IFHPDCr2c$M)C| SVyg(<1\eKz2VjfgK*9#슯, z8"1C: bjaO~[UHEd"( [[NUg1gb[lž4HQdt%eubEIeKE3(|ċ',ዣUĉ"nQt\B p"ʡpPeIC8KkݑG5e%"31!Dx3$D84^![j!y!Qʢ"\JsWy^s!%!<)u]02ӹ @BN_P/4%Ca8Bc{bAA9tࢦAT *A5AP3 e CV,s^y 3U*RXC |spIV!ljSuAKkD*t(f %0/[A"eJBj/TAGNμ_pC7_E&VS:$ j=5 \ p\T,_pEx/3/h/p]m/nA݂ٔDu0hjY}*nn? b=ᷝރ.-p[컷i-+p -P*PYv=@nn2n݂yC Qܪ0T֊] ᪯rJ'e& V8f=ugnaPm 6vƯ6{L!72khH{w9[ I,$xD7z -͜NzdPZn-Н:iquNbT͂<Z$. :ƒbݹ>/$| 9[vfX.DqYd 1<+XAY@} *2" -"hkQ+i[Y^e#VbwiZ@i!s8mLr/0hLG4+yS>XtJ+鐼I5TTc;NO):z:RJ9}YFJA~qϩ%9)(n pJ wP|`$l.]R0!-t4Xk]qi_5Ieb/8hDq(hѹQ] c/5)GlkȊ[q=\cpZDuk`OD[Wd&dxʕ"C(2]9GbU2؋o`'p[˯cՔvBeŎ(儡@71t\+w84Y&ä7 ZmH^,(H%̚`%LLefx5WJ~̦R0XO)tKB`ꀃ#唺Hp6͖@LX'F+A =!L7R?QYO -ulS$)0ZmkgI@j |t1GKnϤv8ԹG}7l},`nSh#@rR tjl5D+F NcB3"X>#7m%Z͙aB\Թɽ@rv"8w JbPvge"x\!υDގ3e1d0!@ȧ[imfˢz*3І>Śh7sl5 }]O[!'q+yzL(=Z~.Ԃ) *s - Ӱmy^A$c -XF`# =yZu}`y9;2dT!% (#NZ| ] P Hp 䭍Օ?l2,>[ g`H"Ӹ-O'Om xf˽>: ("5|[|f32)?8$|2*C}`m'֤JG)S1r{>[F>P)[t Y1 O/a'<DySB[Rkz[C@j*뀴!=!왨žcpqiҁ`nQ} )gKo.h.ce<3'#9tVSKkO؅osԕ4)z ¤Ri - e*{ -4;"錅.p`=9&6FڡFmmyq٠Mt.F".RI8e.q/FߧvQ8w6ľZWd>E$OeC' .+PX%* gurVprtlp@Qx0Ru@sfI7_pAJ.`a@`+_6 =#dZ.=ɊN~zW}kj 2yOj  ϙ@e=^* - ]D8ېDVj+(8ڍpe:EC\0ŰZ6BIeYQv*("AW75C>V>`JA"{4~6Q7k0q HD-ja4lXMtp[pzcpaxP9j]ϳ]RN^ F,w$_uÞ=(/uVpǫ?( -:@9O=)vYs|wnRI8/țx =ڐA,:qOOԨ;9]E%8, !K" -5͓tuʴ17K@p,=DJd0T$qY3=d.:9Zp`7PSҸ"ѱ09* cfHR*&g~Z|d+2GlDF 4oE*6I+ٲK#lJ ZeOx9 `FoF8c'er HH'S\#$8żQ݀5,N%}%pBUvԈH7CXˍ/[q/>!40V)D@_PG41ԸVʹ=zEF:7oo@WH5\ =Ps7[@ZxixiG~MuW/$ʆ8G6'Rs7@s w9)'7&}^|{3@nSJ` l!&X+px8e2@y -He帊pTk{pxZ?,)V %X7D XT70*4Qж? 7ͷ2/n% \fC3ThZ3̇JEr ,BMp@:pdV9SQ`vSőU}t:b %P5(-U ް6Ўp$-7%!EiG0N7pI_z, -|S4>t`)<7p8Z5xī_Rok5^p2P>C$BNB&.7yU| C܀٠  ->2kSnc'v]{hWnxu1`bK&[aRS/ARLAyI]0pM_0.5wR5^<`K-\yqU%E8q*nΣ֋B~x!E*8[*+q';ب -PGOi&v 9q#0ԘVKUk5j~$[ϒhORHmy[ R'씊ARXFh2'0eiO w YOM@R-Dnк+f+i|ľ>-جL@߇-K1Ȗ:W PЭD<#%05$y$ v[$@XtO$ @L*#P6kTWVʦ3fnO4 ""1R=%Pi:tHR^QFhˣ_0/Ҝ6H'.SL&Pa\uklE}9+Ynv8! W'yCA@ ڶڮqE˼x -pPsQ4G.@!yhy=0̐,"Fv R1aSG=@B+%b*9D$M7r$$/;@\nxf:Æ:ۊ\A Zn(CBL9I:.k{51V&F*7qQ(&}pjJJΰ`Xj@o \Xͺ?CPOZII+4m~MЧM- ]5|Oj(Edi . UF8!g^.>ʢ4PܴZ3|"L$ggj1w P} 1aJe4aR2#Y+[e'Mlj*6I@ݐ;WY0FyeRJ݆Mݑ*^heu+I^v`2`['7eA2@ff1l oK3PB%lseI*0IBG/~3՟pv#6?3 V R zG}O-\!Q$so4ɇXQQ{VK" 7B E$٣Ψ!wL '|Zŋ1&ص, *MˤO *-"V:'GV3*:=bGM[/ щ9JpxE/5!1+zpw8X -^+|0}W9pjGT78Fw1жs0+`1 -05[5+4xB`z#v^pѮzF'\Zf$?F+EYVP_⠨|UuH*:0E.Ork5U;Fb?\YU@{ˬʲTb- -8zK4L I&M -(pT\㶲UHb#਍` -j@yGW Xqnjڇ]'yhNٜHܱ*@ ̈́bjY(ĥfYh"jЦml$ -Eq\ - MC&3u׷g&Rgٞ[(rU~bvZ t/WѬGrU lh"~fCO())_ -0 -rDӀU@_\xxb *~۟U@lļvn%+w8|cʍ)%* -(`Uځؠ[T@n̤Zݛ[]! 4)mͅsgNp"Y8&jsr.&Q@ -Jǰ8 d ؿG} J\_ψi8J}+tIuUڃBRh>lΏ0}OY/Q(,YOdS#8cB ޷1bQ=^}E$XCU yhHԇp9cFi.YJqj}ũ] -B:`f dⲋ7h~[{/~Kٛr@ $'|r~  !FPe ` g* ( ͑kԭ '@sP b4.'`()mQ^R6yh?8l&+Yg%QђH2 -7kdf*j\"מ'(p:׫g6#TS?J`i66@uD@ӚcF Ub,u{]-JN|տDa{Qt@3xS?RH;H].vEa}>\9uImR/6Y6lmn AKQ?,-m̸QFpf!gш_̈́ }>}<1jZ䐢36_/ -gZȿ: H]i1Ook}I@>4`&B5(JHE`N00զsG}cYZ1ey6)"G}LVЅ< (x* B|@!B3&椠{@^s=!ޣ.|JGgyh"o?E[x#;޼E(Z>B8-kn9T nY/KREܒБKQhᏦHBV~ Q!eKߘ:JVɟ' z~< ?Qa?) Z{EA-_ 4v<0e*ʩsGKz%$0'˩pOtԑ4`P$CW?}"#E£@DjT3wȿ+ ”DH[h -W+#xcGW)y>SRe5 yOŨb9<%BIvvJ8o\c>_ؕ-;sP|]A IvOݬgM/>( Y_gbLٿ;U^GY+Tم? c󷆊0nl-r3 Q?X+? qe1UY0: /]8v_Pt\ѿLi ̆)I^T3oƄG?AH٨lS"ή=O|0 `y{/(Upv;n7~4J [*=WmcԕܣLKݝ"?u<~4ZRa+H@KH? 2$N k6 -_VzJc  -' -ސ(.}bNʧ0x1tǎ!c -f- \%HZT7_L aOXwy.Oibt[ EväM~q Mp||~ox/tg5owG /Rы]'ypKFUEx:la?_ Bf$%tyof8x*]GaL'3WN""܁s~txA0m۟+f`̉ %iۿ(m.v3fh?>;i^+[iLd@$@D -b+G9ؐeZĕv[?sfxOJ|} I0PT[`*o7dǐ~7 J&qӟe1W;0;FrGmFMKA{dut0J -E?v{q,[*HU'/^{{OϏK86;/xB,!sI9U֐Jo;ji{^z2?I01vn0?Q;;ѡX\ 3Ub)"h귔|7g6h,i#X}s' cKII/ZdXv}ϪW>uW߈ I_p Ҍu(tDڡiߥ1zn+/cE *kL~n : -;l;,sE~.9mD{bFtT?yr㮾UDϻGRʎE ,>!Uq8Ie("࠹{lxHN Z_]H96n3AZ3 /`Mr\O{cwos֭e'l߃??E&+L;U3whM;dΛ -r,ENb߿Y B1]")0YZIf&GKFe8pCpʲ`n2Zac}m| "HO\gP"~gMwlh#ZGTJ2P4Ug:g^pM՟^0u<%G> >3Ba=s,';t4DQŴ_]2)Uo|JQFUQTA!ҳm2l'8IUncQ^)N4a?& pcءrs(M.^]T<^13DىD{QR M: :o&T@$Ee~~߅t3eeZe9zP_?RS!5-RjQ8> ìC@q>D4A7?,uo2DqT|3@q\%Z`Fq,df>15= YHc6mo|W(<;O_>"GQur-N5ֱܧ=pt jyє*^o}݊6o? -z Qvx_>zDixPYPϧWTY@yVoZ'*L Ԉs|Ѻhoi}\NpgcG Sv.._0?^+'rOm-Ρ4%š FJ l-MOJ~ -^,5#g(/&0F;G7,DV Gq|eFSw/r_46.4:o@%~jgEm Y^G ڀ108.i:N7~Zo8P!Fķ7CX)*(i:i|Aߣ;@6#)sŨ| `>%{͢ВV7_qV}!~yHkSI}w.g{MeW{T8/]Ê:ս [OB#og wr?rcu\)̽-~;l 87&^qoޙB؊p6G|.y +Y@;(4-= Qo!4ؾ~S׆tۃwxu"o-^W] E_Vo#q(]_&@fPw])VG -w~9/<9EϺiaAɱ\aۢ(?!Ph HPdVB[ymHk4q;ZUѽZ''p@ a:l#mM ; 6ZcZwm_٥|9ENU -7Ն܇#Xq(lzw|?k{q ֯gVu*4jd7=ij{+%z-pm|p- Q|X3gl1|E>fk{ދ&e-}K8d8UZuñ-ѷ'1=ͦ[ fӟJyeatnc,:^/2+Rck{z`VEZŬ3W rVN_T0tbbrϜ6z4ӷ/QJ!/*Ki{Y CR۳uryھ@Q3m3pة3RB~c-e&@9~4.Mm4Mm_.g^P;cZwW۟*Yo%cՂ9pٌ$~dax?cZt yRF6頮 .PN'~xz+ȌT26!iSb)rau{h3ŪsAZy藒G// ؼ]1~4n]ybQn.o$=|RBX(l6kb23 -EJ!3x#u* ,lZҪ+pJ'n3/efWX[R0F{gJ_.t<;^Lt) -à.nHcONöHQ6 SzHy*8cl7 -c([ 2aU;* ʬyLVÞRy :>{W2E+ 1O"L X#UUgCD~ҿ:_W֢:/^Mt2\B$pI>\pCǘ*r {$q>%'w~ǞawkV$y!feuˊ;dsHgu.]΃܂VTP[U:m[2x$%e `l`@ -(Y<7)ݕ`O)eW:@zc!ْ\ k7\[y}iuZ]rԣSա -iuy۩HAJ@&S9ђҾȋRHx`Iu~F*ɏK$)6@I8)Uk3ZykmS(}{RÞkH)%^`TT)%e̷g$+ )tJu?JU^7T.ُRb`B:^oGx&8/u&`eQ -~ኆk`?9쵕s-HJ+hڒI fࣄbl:)$3vRʢTERH)슣Jv8#jQFCC6GpBc#W,Nwql` ))<{(}K_CL*"9,7&+l,7J>Tv`W1pPEބRO-|DEX]EЇW}%[2DQP!)[eh^OUw*}9y}[v46TvkȠAxspJsS͓[.ML{ݽdhKLO8P;H"ns~ے֫& -'YI}νgx$  єpp@{k*w7TU{Ǥi N{AN@S]{;P2T%27 kz&NQ'ɤ d)nΣl6}Z#ĕL) A̯xTݝF*8ĹHMDJ7AKR4I5k=( PzMx`\Njڴ7D~ayEy+Ab 4BZ7D"Mn)G  j\qC`\{9:# tϞ :2ţ(b$ ж~n*ؚw7 9WSQBpd"pY~]m~|+VBM-/ Kbq;. E[8,"n򋀓^.T5:.gm'&~5DYdXAǁ,ASY;Rw6H"fC-H$zMPZ?H!cm=/?DOi)<أ -Gh۞ -˶ )fߞCFTQTRFbVP]C}WuM466YJd4φYA}y*5eALMDhY0pLj| w׉t̢JRPB^V3'>^n8g4uyANTdѾ\ I5Tp#}v`4 ش&1cdbu 16ӑla$֞ ΐ.|FcP8Mc4!Pԇɀ5Ϊm5y8ٌ,bfLՂGb*kAm.ţ;-lv]`!;r\@ޒhOMgaS`>Ð]\S |p|M4T >YP'w F -(ltw CH<1lЃ6m74OG *ize*:_Y_zϯ!뛌8 - ,Ps1+= Gc }6?ؔBN\ Pt, ͦ} m0i>^ސnd!JCۏT>dDg StEĞN4 b4uЗd{A7 -:= dW(z͋4~LݥMł{AmrdYn8b. -69`=2GM¾9p/A(u谠( {cnpU 8QYZYo:‰) 0[T =btMO44CG>[ܤ~vIQڞџľ2>(m_ [ /4B%vw7UX͓E Q#!T*UCR͛/h h7GDh;p2o2DBg -'Be^ !B3Z+(BM~>B $WP68e TuyM\)Ub}nBtcg;Z !xE 7 -yniqWU)M ;&`?E-E[UNeބ$9?EgiF's_>otWhj$ DWSS[ʛN lCZGֳ"d4df* -KЙ~7!p,Bm kڼ='A/>w`R= cqJjM{cUfdIN&䏫ƞ&\("^n_KehЈR[<T:@/HBm6>KCH' HD+?,P-O{QB@n"&V /ݔ bj{:.ΌBﯴYwSB +\r9iB6+[2j0rr.10ȆM302n1~Uu”{t[A7O*¶S %:VmT?&W8HFT)A6u-"t[TUiO -/ee1"+P??Dl | !`6j5\gݻMscVL> -ĿiDD&R%o(JP%QBD q7%RӠj#-l&~zz<'n{;LMك8 1ƄudB#ҿeBHۑ qʄ 0]΄ ox Y 6BvpY489'/@~S-NNR?C'< H_rN(S4S"[JaK-0>lsi &I@q7FF|>s\8GfǘApsFeQBE4"jr!c4wTf` J󎺰E#VBNNf @O9oMW%le9 -͠Ƙzk,`51JO  9ZXư>Az+8tԘC1hibSB<ׅZ)[U3cARVI$ EU;$Q)P((P(w Ig3L y, TLx0o9qk $bzw?-'#z/(rmul2ptUȱD`{LTMpzS '~L -&)QRRDSF[E 9wcJЄmԡKgQ% -SGK) ֌Қ:ASyXb"htmlIc nu+abjAVˆsƑ n AS cdrl7}I{1 TDYZBLut4=fL𺡓 -K=j}bxQ0yrSR *T[1!yQ2X%RNGծ)BL![Sq bO)'1*$տa"\56IPĄLhZ/u?:i5԰IUw\**cB=&^GS5KI)ǒÑ$&X1q<2#T "Kl90XMﰥ*2D:JKr` -Oq`Kq-/)U -c8>p6;8P`)U9Mn4&`e@:a*q0gK\LߧȆlp07L_LJ8y *ޥ׵ҫ2+@uAeIU.Z1l|.Kz _O<\2;1B /Z"Nj*C]l'<\!+xOg'ʹ.~v"KD- QAWe2){_!OG#uߨH(sNc e 9Ν2ik<,ns&S< #$;ֹw - ɰwvԉӬ` #)$N،WN]^zڧQ/>wxQb:&p%UDT<^EX' ~0)GAމ-G각iK5ΔvRzJKi釕O 259TۜK@T+t+Lbd(]bJ|tRi1.͋K55e'+K>^DT0 ?Atp'0&>Y2ge>/Ih>stream -`[Ȋ 1#Q#9MZUdW k`e31 3&U}O5(:++KQOzH0pjVuъEU^ӜQXHJ5Xܭlr%ͨ~fͨ*W:h2iFAs٘j)j\HgaեPkggKG ?Q;1N8? Q<}h-*wrttl:Gjݚ$+fVa1 z(s&9qU]NFj~a7guR`5 -|{I(:wօU -5 -8PUkp.T2|bF< q(޿BLIQA0*lPQG=/(J݅5ڱϢab+!ҍb]t0`cꪜT^bAXa4 BG4FOߜeƥ=0rL#̦!i/FD/WuP_yXAuQu Mիu-niG]hW檞6BԻ+6Dێ}S$[C,`GmQ= - JWɡva(]7 mRYEwF yE~S{Y), y?*UpAs !{a1xY-tUϛr%٣E0m>INfяuI%s) Se/G{"oXv R:-`>\M(,d!"56B3Hh&#"K]Fb.:dYѱHXsIRƄV8$n)aTcWdTYmm,`c5" R^mʭoʩ9Ş#k1c-FjS"lX-7iW$Peh| \%, W9f/bY -6Up{Y*%wЕ9*1qN\#GWp ]v5Ç@:\7+eW*tO#wfȢGv!_rD{maXa( X,*0(9L1%_e4ϐRLy`WoW|13!-fݭv`/ظRfIckyC2{*N*jZ1U.I9E4Qe=2o*[!@R$xQe];&8zlnvC(tKQE'mE I+au -KHdt*7ط/RA>?-4J-xﺂг<qM6U1WJI RW{T:իnU] -(fzHL#ϫTU)ȯگ6'xrrU3 -bu +,!|$G^\@ý]6x,%0,QŊS$1e\\`%1ti!rO\^ѸWhD]St -E2wc8hf1A+n~IΏxIŠ];d3e^dI]?HV_'$Vb߆Q> 5 cAlCYF8>rAYcHe"6CgXW4u>=ÅcdMD&ne>!Y('e)l](NV%G,:c(x(O: `m6]xv#E\Ydgy1}X,gM^0TϰLY1g'LcwV2{ٽǪ;|ߔhDAw_1!ʰ偫wVqNRn.Lg|1Y!#Awhusd;Nm -/Ck+B9$*óȣN61Qay,Oq=j8Dgw.:,'Y #x7)Ugy| 4l@"IXYxB%,S? i9r?8:Zg,ccؼAgIiYԲLLo+ɳ` *8, 7G%|.g=뉑م<˛`_NRbdAնZi0N`*ςmGR1|gQk䊞?>,UT5vR˳p̃q'N+I[沠. rXy%EbgjAu*_ʳ.̀Xx%yP; 2X7/3 J_gMNֲ2:k1vY'QN}Ig3pAj;Mr%(̏}Y&Mų8=pȢ04&tQ]~E?dzx# MgJ6x4ςcm[04۔2V1 -Q&H$ guf Kr‡哎5?ʳʐpҕg!ơrSImy,O_]DzDiNwwW\MLoS؇hS-qԀDA;c, x?hOv[ ehAF΂vLG){Rlu؝UHQH͝{n`5Y<jcHRxYGwcC!'Ԇw.#4tC^; g:lJEE/ -3wη!SSe0O?lfbgh6Av5а*Ui}&X5IgytWzy=Lyv_ 8ą>tV1̾7ɥ&[X4,B)ޭPʨVZb|MX.+=uVR(+^ dU@2Rp#kβ#YN-uRֵ3+㳣42pQgShHU*_B/ $Dβژr묳:yj Y}f:BNUYdK!uMG:k`9kuA o,6=a>6sVĻğeÜ.ߜ!bYsOQ,uN"$ɼCH΢k1<'*Nz/38 U8~Jzh*Beg؁dg}6;G>ݲKߟ -GCMO+EgQF&CcݾY,UR:C +後$jaaA&71i}LTMn%Ί 0O{kmWYN0Cp=~o\qg>xt@(6e8 \@U0Y|ZȣrV1{" loz) :ysR>QwYE&AYO&&aWcUeRlz -wMaxsX1ˆˀVY)V9 -AIIľ^_ o -(XI7/UV LRu\9q fe,3+Q_|g!*/vF-κMq!*Ok/#1r%YQCYw˺F8빥"vgAċ2Q5 Wdٜ́g;xy g-ᬷUabg+EyݽfE<'tbڑp%]EjVQ<3Qŗ[xDT O,/RGLrJAeIi5Zה0M ֬O -PԌQxv;["-ҁ S,~O'qnw⓻m͚75U6(4_G@Y(3KZ/dWjs jJlTRMי5-B_YT Gp$<֬C]jmfwJVn(tɀfقz0LSͺ;f\m QIx‡9:EYP=2M?L\rQHrV0!L2H-磘%ܨ,Q Ϡct^6_dG|Rq~z,WZ;yjR -LRe֒f9}775ˍ%{fݗSRPa<,{=\$*4 R5|Y|L'0fq02=.-Q4+a LhVP Ѭr]n>Z`lѬ -6Av/Euratz3 UMcFH+k(6(4 +j$.Ci;0Ej4뫁Q\Dptp>GΞ) Yfa=oY5-hV4 s0:+8,Z@4T*,/ՑG0nۣYx*Ѭ+GD?eL/*$ (뽞rMiV@d{ʒmJiIPs%Y8Ne0c3kg2GH ͸'SҬFΏ4l-Y4kvּ.Tb.e%9l98uxfe\g 謅DTDn,QhN'Cf'%f pt6@?Zd:|Ga4 {Mi Ԭ $\}2W ÑY:ڤUҺK$jhLAȓ`f )~Y Ҭ|Iӯ?4{cj(KҚ,dQYp,cmz8Qi֌Bо6JU^Ռ/Y\όȺbz$V)D'!,cBK.V&nqZ5Q [UږAuN̘^g׬5i LO+(JKp7CfٴD,iXdY+ugcqU{m1XߨY[ig~3VtR5K"jHߘ/CwS!84AGRe-f|)jB6"'VY͢v;xf9^;wY+jq\,?V3Y L9]ݳ),֖xgzY,8o?4Y{OHf!5kւA{YG~J!Y_WѶУ5\hˈ6Viܟ[Hn?Yݚui.w -JfEOP5gL-RKjYL.o!&B͢|B 4OTtOhC)D% -i_WpTf啰Ziz`1ͪnrfi[;ʛ%%G $&zլ{''jj .,b<Y.WfY#56!fZaR557#YoiV.vEfZ 5V?NMt*9!%"%MVO$@%(/[T3PmJqSxa>3V2{)A<d -UK|:IHYáKD}赔)|4w>aL=@(j O SUo\nfƂPl#gDc}k$d>4o381"aEc/`ZF,P+? %\-ÇM$D9 ˰YLq⿜i T}9Csj1k})nm< 0|q # *T" d}w-'mo!E)l8.&$nG`8 AЉC`pZ 2~C8lC`o``D *;FpDRMDžL|3&qUBNlN  ȃzQ -H70};JRσM'fC -# aGHf23gY5GmY:rV.rl`L-6,$_; WXC&g4֟hyB!vG`y|·F\Fi\. W!Z!_u3 xe[g肄4 5eDhϡTàx5ģT -وe ![:&dϲ\0CXX)#|6Xe;`0pN_a CiT- A U$T63E -2WHN|eؒa-IQ--oL -,/ %?%/ɳuSSehQJxX=܏3xp?`3QPϨ# 2c(]6(y!p+AԄHCQQ4ERahEeDևrI(.B/b;|9N>OğS\ }6~H QG Li7F 6&/1@ |Rbfel9Œ!$sx~0./˕-B#B -T,\,*HH0)KU8,0ar:.)c]?FghRA^ɠHQyg[ 2 Ը`Y ' iǏ\)lZ Ƿ:~8ꆡt?<8*)4e z0%PBf/ٙ0c,Ƭ ΂㸰0?dZg4ӴOAs8C;DڐESA6B,RF| >f Q }Z wK"'tuOO [:PBP%,!ͧ-E;B!.Dń!^"ѩ\\gE&qCXҘd(Ӧ7,DIݛPVѫ6bE"z=:geabJgies/c_FH#W֢9gspp8p n*^ - BZ8\dD\&!:jhLd2irKRy4lZ-oLmR =)I2j@߷46XO3D4ƏOQAr~pNƼG GiAQ7Di-HÞWҤ-ƎΓCPf(f\TPH㙔V9 $_ -O @"x<,#Vu@mhXBDj~m' rvG{j8& ^Ґ@4l@ t3`M`{=|'i`,EټB8ԫ'Հ4> ЍE4/HC@]~AO%Si趱H9?«*tw'LxiA9` -)4;$24X/Ei9i<*»:aҰ)F2*R;!cSh>j2XlDiΉ"Pr|œ>#PxEC^*h'~~&2Xw25AG[AߵDg)4|st h̑%Rk3ҰW]JX@ xLI9x0Ƌ"An-p37aʞ1!֎ ~ ܾc5WYܖǨA\Og_$'!HATӆE=ܡ5M,4feZ?׿#2^w0OAaPiX]х 2+tL4~"8jҘWӻ;TAoj$KQpM$VJk#jCvB܉y%JZ] iēdMB -] &ʋBtʫ/ޑEG -.pgO=!Ъu(=QH\4֤"j1h`\vE!{м%ee4djn nWwP`L=TҠ/8h!qR9J@^7踁-CBU&C=Mτ44ѠBJ4 Br AU#EVҨ3T#\Syba!^Ih%Ѡc3soZd ,tq՗#:vS$^,2l97! |RL! -/.nfA!)sf;pQ}mB4 -sº?=jԌGwT̠ZsnϏXY?1ETq40QHA44i &4LQHt’-a1gRCE2 ' Ҙo84#癲9]Z%eB5hzgH#`A ]v(R}t+! >e|ӐLc4DDO! n*lC~ -,RzvÁC}x(Y! .(-,bd2Vhy$ۡZ峤 i`Ҿ#Ҙ f%7Sp4ۮ&"C=pp)d# P -v/0v0Kp=dž^q6AU&hCo4 ihiSsww\졵9ǘDWKe;:N(f? )i[fª^9 J0aj0"Hc/qcA/̍!U{ق4y Jh$dC4Rdq?s UkqSoX<) MAS gkl?H#4zWx4X!:HÍ)2e]8&;oy+(oЬX/Ait~D;&;3)d5"x>i#݀{Ƃ;;eEڱA1f2'Dϖ!^&ẄM5!)`<qv祏e}P&NyS,g|ѭ_0 :AQ.4BR('vm~2! KoBM(Jo4W\H# n<64<$7D@ \o(bHHWA35Zzм{IYH5DlfWO-A^BQ{}[3 iyr7 PlI744vC |! $BvFn@G7BԉGHk.}73QHj)aˤo@#a4>•6 -zPlb{Hu iR,fB;3ZRe ݳuoT -e0HQ]DSqu2\ B$%! SO}1iB]A&!>HM /d P͖10͹a" 'A$42*hAe#&h `}^Q [B3H/$+cDm2:d鱡")KL!UHA3ѕsyq,?z -jh\84HŸuk%VxBNA0)ލc Pib3ؽ5鑠x?Z#h05os}2!a`T ^HI6_JəGB3я>؅1w1ST:RPw?sCWM/86sm|yHGohPI~]~o(?D7d&4>| 4QO8WGGcN B`يoh5_nGa[ ©#?Rɱh[`A7@ÒTqKᝬiE]C Z iá} g~qWu|97A|: ؆ l El+FV*OnTn2ot - ^mi8i/+lSU1xKKܴ -H\|N I4Ӱ`:3/C6 M;CA"p@814({}$оVLV*)Ʃy1iP8yf^.HiN ވ$SSUc8}x@2OchiP s%;р$\D)4?jSEXXSϢ*ShHRbGczsy^Qphk -Hc.EJ3lbت( Rh%"3pG@A%Sd?Qlb88&igPr+ϲ'h<9iHr1T^ -l6瑥xGC`"-? ?|:z4jA"<@`螺 \jр΄)l -h|&Hm`U43_Wmdt GCвV%[j8 tN Zٴ6  )[œb0dz4hP,l/eқG a<"5;\d6e0uzE)T/1\enB-E78^lf@GAR%|4~Uܢh hpGIA80h2~eh[ڐ/ Wy+"ErZj.v!b}o Prw\wGhJ¿X ؐb(VXX[hX}G#Ԕ+gIufw$rq `ψI*jP|4ާFe0ۯG vGc&CMf6.w2T$=419jl ۣ˓IE8bY)ƕ P$c$X@ǫ3:I2l|ңW\5  J:`50 -l}ף1-]O9Yj}5~eh>.CJ]Ь skp8WL`iScYJLBV_@8 Ymz4Vj"r*qr ߣ/q H%]Ѱ\L9οjœ -$"d꜐24[Ii"exA17 u 8+|b|@D'\nU~u*:1JLaArr<]ipL?pdG㧋yE5Mx -XI2[ᦇ%-~9Fn|&eiOuI X'ekD '[)&?BhhC G.uB ":@1yD+\5Ӥ{׊YR2\-УqN:)O&R몰: 9O\d6h T -;GįGgfѸrAE] $ROR!ޣBKCiir>i@ʒRh.Xx,FDOjJ -ф&>bC)_*2>>SezFĔx=yty4sl}4IP$0Lx$2ƦՊ!F5iG0ɎI)"f4#K2xNa`Qx4lp!_ - 6E)-)(@_R|_0]klxJn5dZy`T cEHƣ4/F*J&ˌG \Y31<F\XPkfT(FZ@!9 ֟Gf$,qzؘlh hTD;T; c#8QyD%Zn-G#/y']4d'`5yT2\|3.'T؎Ρn}Ñ:~88Bt4VbTtr㣏|X`j[JvYYpcg)iɐ C j?qTd0F~gr͸ ?ۀG#z%tZh=NG#.ߍ -$!~ոp}w4pGxR#pJʕ;eM"/p<ܷ9VGzLR6?ϊ ŎC5e^F p 79#]UdpX -TdXwஃ]7ג?3zvBPGr`.(x xϣﹺ <_ pojޔIvGFI I\LQPw48sa NE`hW~xAh?0b/` s2c YɎ(1Gp0-٨ - woшBJP^1ј7TP P\:*Ůׯ~Z'9v4{v Ձ~tCZ0 -;_Ǖ*\g~*C 9oG㗀;CndG#%w{hOihk40@*.׳|G@hB^'ˆ+hЋB˳ievCpupH"hDZGlrɊjt8u^nRԣ:µpO.H-YC!=S іpILP_փ$xugk1aŖ0SyRhoRT -1v=rXH1Y֬z4 c?w 믜@ţPQbjRa|]@ 8dyBј) IvE -L)<!z:<?J Lސ~GLnv%ۊrA16R:;(R1F|v,GCp^V+Ў|TZJꍆդig 7y9bC^>-@Z T"W7k|+ (')N\nz)I Py /_vy9KP:EOz![>k1B_SZі5XA&{Mhoz ua CGGh{z/fE]Zk%8({]3]ШST8̑);m4ʤaU繣8vRԕЅd;NR!!P\5}'I|TxKع5r۶Af1F@w3Vhl-P-ÿ2 Pt `qhR 5 \DN|>rhm l4G!t @;>q{$e,+ pu;f1 --ռv߰j} -nAu6Kȗ[`|Wɰ(Ϡˮ[dCۂ =Ot*iغyw !Z{a)6bca8=P,E.z9(b[eL86gβ`w6+uZ~:/_؊s7ATӂ~hALw |5~ͽ,83>`7 OPYL]kYX`e䴍BKNNl2U?Ҙ*sS۵h@Ehh'0A9 J8?hY^Es1+ {ƕ/X7@7 ~%uܨ Z -}p4hL7@4PLx!2ؠ,cr:X966USXK8J`6K/;iGvV’FO\sG#,+H t0EwWT[W_5C2z -FPxܠ -g#\4$]h勮W\\!X7<@D@F}рO.+skgS΅61Yc$ʈ CZ/uEԵ-֞^7ǵlTBʀfvfÛNgxތZmk-1jQ{TQڐImbF?4ǿe^h Jt*n-$2m G-[mH!|y~8^ѰSe>|yw k09̕ v4ZlbDv4榟[zF;'lªVtqrYM֌B~j01=ۄv4^vS`icX)F[e_./zHN*&|E8jVϋ%??P}P)=$~G#V<t6K˨N^`hx  cz\wGlGcێtqRY q - V~QV 'jJ]@va - CAGC]c6%$]v RH"E]Aq,>^MZa5c'l|}+%D8VO?=Ӧ$nQlWxdKOm;i]o lGc~wжAC -/1fQB;qg"xoG# $'; gpT`h< tX)M|ŞZcyjn|mGcʥ@Ď֖@Beh Z%h/WZԣ2zS8nj ԣz g; E/HVXehPGCE LOS 87yĥhĝfͤVfA`@9b˲yb؞ԌM3a'17X1s',hF>4X͎Xh%4r46B -( 46U~n3Ga}-\@;/C3.rl%(xMt\g@j\:\qΨ&gI1˟BͰc0݉ AM3D3x;3fc*3C2X-pD@p-c-,\8m^yePe@q,#eABV( -xeRJH1Wq^0eXMi' -s2=KPM>s|Gɨ 8ddHFzdxFFZJB,V81U 0dVP2.y1l 㕚׍}F661c/= Pe1F81ݍ`Kj1h6n嶌1v1PTc1jHNfcsG/V\;`(%+ƥV14bSUcy)E)<*-Nc"ctg!F\LAvP$bI>3xaac0Xt0h1.w, hHǨO˖0aXˆڃAcc 'u ΂ #Q9OZ  KOئc3WtL1""E`P +l#0Ac ;A-ay1s1Q Ff:6VHzcY8L@nG#TcDVcdm#d=d"KF3٘dPA'  0$ʢɕ-,,0ղ2z.^&L 3 S޲̪>ef%*RgP"49:`FiX+0M,f A83-g y0: Cȩm\vwTx#8B`V=W泩3.q/!FF` 3~9Gs| `$KHd6_`5h"Em.! L8+tzŨŭc۫a'Y:aMQ [yEӱ).,`^A5 -.۝CϛZլj1쫍7+559+2DⒺ1k05]*44㡵_}xʽex~ Z[j-F^d8^ѯZWP8^ޚk7z -)Z2 -] FWj^S"Wkq -_ +.݊/lB5B -E?l֊fJy -cZQ"f[ -d6͔w["Ze+N/d\O;Hl#(UD 0:ViE*`m@Wv͎U!mU\E>Ud *s[!T=Tvkũ@n!4ח_Y݈g `T'dGEȨ976E *tXh۠b}MPf`?~U v74=vS[xˬS@ީ)ټ!wbn!-oH4B3f@L!/|(p]MMz,{\h~R<,T -(R=)<ѤH2(j"Rf5cXplmz*8A(F*B(k(q1Eā(zEQ(PP;՗XHÒD -IdLD!ƵQ6CQ85v-C!2Kq` -An(iB19(f7}džYr' -_{>ݏr6[fd,Wl9E!t9b?_.; t=hSsd -n0 ʳu}甆՞I\SιNE ":ܹs"v4 w,z\s -{Hj@AgAQTCAaPt1F!fƠt֠V`Pҍę.7X%Pn)&uj ʩcuAElA<Ч@@L -' -%V=B2Q'$'Xi>1}u&'neO_]z"5N> :xbY߉ey I*:ƭ EU7g넞ȇCT'`uM:SխnbubTu q :Yʙ:네IiD:#uxu ;M tIvBE uub[РNpv"&uWs8{N:u; -;A9ux׉k ;/;qՌn/;!R]sbRs9Ju>oW(Ĝh]u"U7Z澜pf T%=*# sFB_UWnA b:Ώ]F.p2uw):vSĎ׍"٘Y-;lvNGh| #%e9a²ciogHN9rr;;7'؜Phל!ݏûS;/"xxG>МOלPh(N /!Z1CKnjp -rsbDI+&L޻)P~9#4wO- Da1⾛.'ijrSKNR<}}& Ֆ#b 'N;/D߅߽3M(@߄I&'Pfr7O}gpq~wtK&P(DMM H翍 ~3\&X@!&=xԘx8pi6Q>fxH a9,:KISޡIWP5sE8z-E_"Bo'BÜMM&r]$]=!}D#FnXDz҈c# ԡ#DDTK}ws=sa*apH!C^"-{k<{m 5"YG) Gwpn:!{ -!M!P/$ -ѩwt'Ć*B$| _ - !!0B,#JsCD@]o* p(Bntm>s| N*c/jzȣ bO! x{|}%ؗ(ڗ A4j0D>ZU;p8@GV~_?qI DqH9ab ~Iao})#~lDĘV JJ&LRX  lX%,~ Wt0/ @`U̕K?'~7akKds ~_kx-w-x=OC~{rs?8K'{|pO?guo V0gS0'>Ll_@P)BE?*ft 2?ؼ?*3 lX@C@|6aX[  Ti%ӍT@=PTA"h?j>C= -Vp׸LP`s]f+  Cz1h};c0a>4:Εi+{`z?dCԤ|e\v8(9vxP|Gv]u OUW2H0)^֡ WVnTu 0urCNVL).tw! E.E}G9nn@[cK:jN J`]SuYx}eMIE)G7 &BJAA"; -EQicjA(2cP#Q 5q=i, vuCNAg8dRmh ^O9(n; l[ z+,Df 6Šnx->1XەCOXEᓃ ΃<='7s\   7l?Zcp.cp nL -1^sàxx wI Ne: Uph`A @Lnxi+.q08 N,t̃a"[eNG[04nGYίH85n(Tz&p$Z>lq`vH! 7OcBv!"$-߆q0hmTa6$!vjs0hl83 ;lH56#6Y lpnuF]3â!# fa"k0|j/ ^UK -M KI Y5ԠᝆRiT2 F6 cZ^nfiЕK-l@÷F phkB& gxggu\p -  -`H/r5\„E` B)HBAq@ xЧ 6f|<@} f]̠BN, 8_} oV u ]f/^`H ͣ $x]pY6X.M߅! ]pg]0}K[|օ󂺪. *ƺ`:l̺<7وlt낥\h4u^ ^w]R] -]xO.KXJJv9C]Kt 4OP;:MtE,g.v<]W.hGG ƪOb]P S .ţ }uy@', k/[\pr2s!=\@`\A#Ԓoƅ .LoA`oz D Wv IoB+E[ [8sp-dΜDTL-|iap fH-YY 3 . W Œ0…,БcP~bO o2 ;_`%`!" |yw.Xwނ\AkB4 - -\0 --' . -O_ee -/\0^qp,ngxUWUp.TaQ@tAI3I} -RW$TX1*D% A4[mr?.X)\PZ8p -&z:SmR0Y -7RHB)IW`>Rx&? -XulQ8(H́RL@悘(a -ιPBaLBa -A6Bm.?Aj?[ KONy‘`ЪJ.;-ߙ;s ]B.\C!]Q%$;^ \F.nDj,H:2@A!&P -"<@*ЀHX.pp T@:! (0  !8 Dpp !< *x`BEEE4;[X ;t_8,M-@"P t8Ϫ("Ry/V*hU7ʢR2\Mdeٽ9'\8TJ\b6bLTr -GL˕.''/K':9]h(b>jѵ~{cz'J=Ӈm$7$bX8d2켪h IxJt_wcg~\6+Ӓzn|W޻N{+ݚ%3W3u8!U{.=,C)FUJmZdͫ!2٥^HZ{Uva߷_$ԞVyS^WHe`5J7wE a[:W\JŦ_W3">D9^B)֌2$:3Kt1Q8G2Dfqfߏ=4s36:tqL3J~=RԍiҸ:^5UϢS' *qd*֭L+ -UYNU5LSXuHV%E#k/;aE uS6Su6,%ɕ>[ 9,Rt_ --zxEoa#L8xhe3vG|n![ sqn* ؃$[ܽ赨7T;'U紕ܓݲ-ԧ!ەb3S a -4FJQň*טz#ǮPB%ۖ}nZ[kQJ}tn>Ɩ痑+K.ew8/SDxo3a1ëՋfS{8cyMϔ76yEB궩:V|dA3)/SY4O1;3/t[0˜({VL3Cɦ3.FLP}LѝHu%9Mͭ<G&VtL4HcVs[CZHOnWWqћgSx>Wzd8P)XUg5}p| 鈌:$W|cRj;=T'o5)Ě] xSkMLF?}cucΘh[EO*WRw졲o<%a}5^cvOϵqIb5MeHv'2d쾪vd/ G"21)lΥ؝LUsۧOͩ˹:Gk8ʋS$ӿ-ZՇ#nuXf!q.Gu"ATF2JO:uut$6m3Sq4%{,^y̼yQj|xz+۳ ϦGlٜU26ayTd&4vlb`- Q2*Ag}Apz+.aѪ`l* BIb,|A#\%B򉬍rōEG -ۢs'|,2b'cP`j]@hyޱD_E:m10W_=Z=&10&?/&x î7M 1dD@osmK?t{*Pąj8T5{N)!S YOȡf&C7TO5by0*}Dv%ʪg"ey*0+=ٟFq g֩P^pI98E.hZ/bN{(?L!M%VՐ9:P;j-hC7$u=i0Vx?r] -?u =*TBi\nMfʛyG+*WݤsA 'e-uMU'IJzj ~ό ReP IX-IT'4.ya$ :W<{Ϥf<(T(,ݔ;0)Y/t@tR2ĭQr?IRm|>"tY"6) HR*RC}(:INߚEJߘ=I -R|%(H)q"tؠ1,Z<ە",YP&%~NvB2(58 63tj=dR~G)c'r]-('5SFn d؏z*GܓY#H3@ -e<Ѱ>ͱOc15jb.\'4g?OԜvz8\2' --đ}s-a*G )3f<,>ts .M4JvF7:} IgmTZ$9\Xli\h\qBs @E zHԹTB(o’Apb#R{:.;#NIKqpC TQRxAV+'4NˠJbtj5ľ3 DY*6; #봇v{Fj!GV>ѩeRmG$+IQͩ`L)B V#qA&1$q"Qv3;Jjc9M@2%|Pefh}R+C&hd}S6ß g-LIt (;\4.02Q-~#$9O44}5g%<ev 3!StiP*<-K$Wr1ܤm+v Z%A( }ff͏0p(b! 7,#^`8kWoMerG+ g>;u!rM{M\ YDnPp]<<QSn+DKTļYKM=?w׃QO0~z>Ϥ }#9%Z:8BcuAe>h%ߚ.'zLkH'}̛@yαnJ=:K6I>DA-:Xh +ᏋnJ -ވ0yx7^ eYj<дݟv'`PSٯ:{ t52[ 7kjwSQx9G-wunjhəzJJkWIifV,תCb۠ڊBO&6˰tJ*?/'MɷO] (1[aI |O,hgsGʋ -$Ϯa㋦G" 52&o_6MZz"etrLɣx`/ ÀF*˸<\0*QaƏh"؋i hFYH򭳹9#}ybDl =8͞q\0[KT<CF: u0ܞ$?dDCU4bE%Л"4꓊Aii\5ފI}k:JheEĥ - N6!/ kJ4&W]7JOxxjO{K7]b92eP|vI?)sXdLT{-A_p$\x:܅&TyଋRojs~/nmo3Mi3OԍU.δa` aj#E/ }ם Vhgފ~y3<>:aҖ-)%FC$"@|^xQvzΰ"%h:w8\UyG㻾ˢ&qzCibj~X便tFA|,$Yə560>\-VJ]vKN -aF~c[G`>ZzV8y5vKSn QjHM ]rzKW|;}>({~I0_SpBCi?T(>z!^"#vdQw@By+Wyt L-uHӋCt֖ -Y`A?R?[g\ V)=VgXb|fFJ*) h 2=MC~qQn54*h{N#Qvۉ~⟜ pVjx7X*%Uoz#b[} |?,?]V%:`e/|A̓GJIy9;iYG.`R#1>L#ϣ0nTFr]q/2Nݓ; l^9 -a ^v:4 ^VF~\tMA씡?`qrIM(΅)Ydc g;iA;JZ o\A휞2`5qe" @b@4pdT"4zQ”52R-VeLn`Z 2 -ʁcB`n pcv.a;Eۈo| q?Ou:S1T(۠Qe ` -{\{=eTgԌy7/ f}r"l+C牍iQaw˼k=pDvyHz-ҩu{tϑ@)lZxrvJM1Ⱥa؈o/gEN" IgnR}Bnjkm` s!VE - 5㫞`5ms -Hf jI)@EAd:xM@12u\8HSsg,>h};\0..[F۽Zs4v׵J#PЪb 僇占E޸+64qšz{3i$wZ™{CgB$} H#P @ -#Z=)LӠǿ O-=oU5;B]o\4M=5eoǸc͡ZmzRWL*ҋFzBrQwщpx?EC; PW?1 -2pn3qE3PZ 4 ?MS¸qg}Of>QW3~#bǷE1O6Och0u ƅ7tEK#p=.^N[ںfX.ςV[+ a[J^͌i:ho#%AKvN,VGC#t() -?Xz]לm?=h)}kTOpe `tĹ؅܀+aIoЫUeQ]3P*@2"*Qr_pB`FO|dɏ9gϘ{b>"s]/ |S|}A] P X#UBT\8) j\EmAe ⿬ޠA|m~ ] -=+klf(]å^̧V7XkCcװqɕػ7(9_f_ިbe*7:o%,ch'x4YƾP4Emʭ = 9#nZX5SЃHh<9[ط{##xw@rD* YSZ\=6y/qae0;FԀÿDd] $nGhޣ:w`+x@i"rH>J"`SUDdSُ#o%mc9Tyۖ$[i@eS'âJQI9Ȝ]F) º}X<8׬BP{`\8$ Z5&,h FO 6t`"$$5gUvt nMa7VDo "ŝؐu2$m$ 2M.-P|\b\4eVg"R$ZT([|DI}4 䳃j#F(dt@M֢mƱ "ͭnl>z)+I`> Y&]7Y4.D1R#T(KxsUjIPc[Kn4(QNOwDLXF'%߄k$EIL.]j0 c {uD֨oR@hBS=.vho\bpr]sߴB&hS(l&M.UѽwUzjI*S4¼S}s |>hKѓ" ) dygQtR$E&fJ9@8jmur.r"sߴ&3%bjXjiQ{5cÍ>-t23Wy`QH -Qa~kYTvz)tka/S@wQe D%tsnY0DPʻR:`j] -U.8 nL^j @v*y)'d;*Aֺ\Zi:/To,сϷ z *(h#1AHh}BH}!9tdDrCBXM~!MٖwWe7Z ?wu @+{^PK$g+ w:1yqpOdu9$Ev,읤%iǔ'ebL[0λwzrk$?%"q@rP!sK/}ج f~5[e=0ŻOAGߟ"5搌HC3Msh:fpǺu~*)QsƠ4 -Y0%:=L|jI ?Y8G_ݏSTԍ\Ac40?OjcAiǢ_=zfc6>vC9Z( (OBiSc= uW%#Ëp~cj|4֫1<\ؚㄋ:Tw,&G1{i15)|ɠ;vV%Hc_DSQZJi;A]+ֺm[{>~f{l?fHۏ8?g4܏e* Z dJ~"叝>j"Xi;L2@?TcԠ͔Y;@&U>҈\FeߏS[ic ~}:I 򱲃5mQ \3dI"AB(llɴW&7Awep()DSܷʎ}+Dȯ,/c.Jc _Fa6g5fq[flfd ,Yrjf&%,;Ľf FL]EGqY0#a,gzg@ciHqԯ4֓Yוw[7YTzK: !#1Md;@'Z[-KVp&͎cCK}9F舏6\qSv'F$ż&.;v~iL&xJ_ku nJcN]p4VJM+[UZi$^c>}.+ L5nWi<\* Qv=Ey4[@hkǬ`E)1ng8q!u8ݪ4Zܐ EN^(9Aݬ4G'^COV9*r0V/ BӘeNCX6Lsyͭh\69T{.Ws4蒬-Wa%k@Si tKTs҈-:P꺜2Mimox8uu34TuJ.M)@S":) uO3RNQi8}ZhUiuJc֭Y뚪4zT19F34N:CYh鶧4lmMVGI:H)6N(KiE ::m^J#%/`) tSyNGcxŒRj'+SJcqw袼S!ӻ 1zgJCbzdw ȆNiUMX8lJҸ1whxEJ]O%9\mшxT%MlB ^#15*a88]]jgޥҢ4<<(r.S>e%{N>i\)xT̗@MF 0!J(4r, .?^݇LY㝎cQTJg( s?^hyσ0)}/+;AޘE(F8%S^ ^y4f0/y}N4759b$PFR^͖!!6Hz3zvToVZqQ[=`7meEc4~_ջz)4NӢHL_zYϓ,1zOg}o[Lilzzs=bÀ_82z҈Vv[oSL{f6^Q'Q9mK-" ,' }N<A$T4´INM65<# w]I#P:7h0iF% ᢉj3{ ̞VL\Dӿ%=JN% _RpO%&c/Wb_nCFᨱ%În1i$hϝc"u7=%+h(!0i^cg.iä'a{m}{Lj{wy@3L=72kOư>y |BeY׿f|#x|n&x>xoV;(7i(rҀ{L|i9l''-3O#I>EPvF({Mܿٷ ڗ'J \7 -sT}`( a |{|3{QќƊYWO#uI;Hd~;h7/T8Ʉh~wkI,`PIJyȯ\ʯG=?A.q`72.~~Bi\?]o~`WŸMWY35NO -du%nzXJeAi,'e4NX~`i?iH$[_>84 :` -L(q]!znҘwc`lVW2B'pV 48a= 9ARA(u*cD+(e2xg҈E4Y ҠR'"dd5HuH J&t`Td"hD,²A 2g/e}`|h28C -|"an Yi i#2+ -1h$.1g *ȇ.T >T i24>"5^1dbd S6"}+4T L *4x;AĆ4Y>=a i;g"UADS9">34 ill(^:-4p1  _DuʐsQ40 Z9"u`+i>9a HL K ,H#BhAn3@}aap6Hæ`c1 - 08_jiIr[(ɓ+zJLe/ጃ#w@i -=.(.X`; -/ i]8D[Vߨ .B%QH[[.C!6uAr tф4؃0bsJt5M)q4LV28Dwt/0M(u]x4#]p&5Q2]H'stbH# '~! !jt^Bi$8HܞJ! u){V!i^.8IC UH#OH. -i+P@u[M im_ 54v&C0k]i|d m]W l? -^*m"x@Xe䞻 =  netJ%IH^"s{l%j+l)uA>is\s/ NIdl?G< N]ÏjeЅam]` -HɠHcU p炷O LIEQARo 7]0^iM6HÕw$H\< t)1 B$@P⮴x4ڃ DAG=M>AW]b '$(f ԕEi.=]Ge~4Gd2Q^0 ?v_ш:Fi` E9S w^ oygb//$w<&^PvA -Ɉ z4eVq}4l A3^P ql|4G~HL?== ;. U HLFZFo݇>. > -(.7 -/#ךwI |4؆ Qϒ3񂁏wGcm@]  M=S #=`}48ExGYtid`pMBJ-^񒲷."=[K/X3.aQ54=D=6`2 I{48 *A Vy0x/0Jh g V-ZxC-_dPMd0أ`tL4Eh 0_fٽ W< BZ<펆[%.\`8?!x4k@IGZ[ʫLU1ףYc AO&b*hQo֣?/o!DDbt G_XE -j<{ّ꿠qw4J)*{J-hU-w4<% (P) N"\zZ8zȜsp4<R; n4*Fc, C @MoˍƚUheh:BZ64{X` 6>l4FYeU7ͧ-m4nA%L7)? =.FRD#n4,+ h,EQEE ڔ FƤwOuH"A~g(r4ZX <I F8/ u;0 x4D;-GCă^4@`bB+_hGx#Wtw40̎M ѐ'!;tuM`P`(v4 2kIN08Oc!;Cs0h#  GщJGv=Y7" 8 :{hPc]Gí0/NhdK8qiE0[<ʞ;;>ǣa {&QthG#tL,?_uxtcG]Tȳl@jA eAsc0~pvQRt4x\/n7G<:ʘs9EP["!A4^Ɓf9im+󱣡m4*Ԧ !nwL(;<6!S*8)hMYGh Hq]G< D=laХt4U3OA>Jh,FTb0ku4ɨ,0rc]qh)oS+;̱n2ai>ݎj00`ycJd8Ԇ)CMꚃ02ЎV?/5 j=IJ)SJxLa`i:hT`KkZMpTS(P8, S Y`* ihYL?pc PUiEΪ׈wUjh6BXvTœ15ij,)H f\l(1D<~Hŷ푻ĥ\u[i`*zc花yzјDJ/lЉSUX *֋LڋfoT^Q"UR&1t#XPa-* -.H@.5%eGf,:LJ# G!BD!Ӓil[%FEpp^XtIWн.uakZivz%u,4_o45E+?ÉO&z)|2,j@N$E͟i)m&6c%mc)Z/4)JRo/ޢfwOI .nMHhĠf!5/KLTV*u0sGg^zE*E2Aڽ+hQ^ͬ+%E뉦Zk!lLS\ >g/&n59*Xn{aM(4TT\'bJ)4>ZDu\XDN@&<1'T$8( -a J?B$,İfDUl -[m;naCP30R{R}Ty>]ʌso\4՘BNUL -xSS gFɼ2݊<Ǜ35> #(ԌU[07d?C ]DP?ӴSЭ"‹+@YAx` 9 -5Vp$J1=X0S>lbsDT?Nȋ\4M9ѥ}86raO٦#cB匴IfJeL_{&UdZ8%u:S>cj^Dr̤9XKJ\''Jfg銲SI#BGg - -k^ǐL,Y㉨ɵքCǥFU7eEJqGCŃk\I"3CH%-j'e{Z~YlI竪GO",iMHmb9QUYͼ vnop"E/}T#w82APHkTHQMJbL(>9MD!.($0SiLˤtbH)BjT1R[B*GU-8(ƂX^idF%AZh3ĕ,C AdC #Z=-0|C AAMP8`eWݘ$sF ${&l™4!YF_~%E(Q?n92䈤NAd~" I͓*b-ҢdebQxbp$ȱ\ EzT,괡pF@Źj,d%jB5CիT%KsžfZ/O4Cڿ8 $Z% x)mL׉CFI 12XQM{0ojbbbWVjFɂ^.ג)XF1d*OsEfV8-EiM /+WMFXsPet!ȩ =mO*5JXE[ ޶3 -ymB%%aK:A"TgU) ^'MOc%dc=9:~kf}7=>KXEf -qSHA2"QpnI7'Asb,88P`@ \()"8xA`jXk8Oф޾L%^0zO9x*nTm|jgTDє -$)R!zl4 2w wq+o`a$F5Uj$A=!1<`2wlĠFZl,AI i1fʎ:Cޢb~Eg(3 <PEI D-GSG}vQ?X7 (R{֜ueJ!WELy7!Q\f&>QB:sӘ )!1 Er *$]#sT(!mSPz`>0Ŗ0: -qNd#ٻQrCi*WTN'xVزg1q^d* Q.#hqdxQE=P*F\#_ blHڰ}k"cS^XS\d0lg`qnuWUSnPYjVP[UߺhfJ;5d3aE=Wi^I oLQ,4垤DsmQmk$H&O={< ':8hn^%NB] qcHS'9JU:#BS!,!%?r6-!vxH4b+V0J m)RSN%* NIVJ@ F/a^û-f4}0huכ -b/=1ɃK S 7\rS.x{$V R4L^Sf d0 ! J)iP",XȃȬAF$P(MR2㰘y \ gЉ0( -iqВ"1X0Ȍ)< -̴U d0#)i&h`a&"L= -B2J3 - U D` 4@2S; C! 34h!K)Ø4 &՛ PM$XHADJ@2k7U@f""P㣈8Amĝ -Ñ0< -WB1TX͸J ^^8(D$lD4fzl\AT*nPf&̄cUVmXUuTQ{!"8gSf*zhnf%T|Zule"n"$%-G,T 1sb8rb8⪁KIX@iSt5(:?HiֆZ/kI-^uub峽`E B0 o6Z0O`K'hiq&:MbB,T0ZOmy1%!d`C\ÖX  x9^t  VW0~i@T?~ONʴS* r9KV^fC |ByP#tBV6L.'DiY׆֔ kK[=fuZ;4} -'\(`qab[VØN[];M*eZK\q(f+Hpoeǽn'ψ*y -mO3&<&j,뤣}渽25MBѣ[Q2öq:Rwx -#vL^bD̪U<o%Eae GjJ>ʨ@JT]TSFkMF`ɱ^ ([Х-:=b~ ]&.Ú$ рy@uBj(S2L -`BBa7za9M0|=@l&}vg\X1F/ҏO{CϷ -:35ڗZNt^!O pU!'Dib*Oui"gzcrr3HDy "0EjA%-s b3]!s':jOLVĴ/ㅮ(O7~2ↁЙ1> - h>1/~NXRm]KkPP}e@g< [lPR540dt>e렃Q c5_; 8=} f2R@HSFI52|!0ZW^ʓu+p{MK -gh@5u:;!4P9xT,{ -%m?I`ļ4'1֥QT4'G&nP6Q*`;ET͕of4GEJ%U@" I_^mж ÍkIdSܲ>D6WiVvA3&Ai=S HX}ݧ'54]j{VYNQ0OB: -pQt -kt-9|tV\bS#]R0LݙgT5}>Gt3GE~cؿ0tʡ\;@ӆM֚}ޒ .d%,(;ʢ潎@P@%^¸chK PNYeAL~BWG~NP[;3 [">\@5$L.|%q3 -) WaEx/^jan@k%:2>[zt|3:Zu{3~=Ǜ לmRz ٰ^b]̀nnӈ,VQ}QMK;ʣ-T -E27O[|yooUIKr˯ CyʞnHmb~2M09F'n)--Ss#g sG'xK%WjQbuvŰ)µuUQ12!ŧy"R'1k֚\ X -Ԝ (Mc!oPyCՄ1}x`B J[j(G]CWx^",WۺkοQzsC TCYU|^1[eN?saJG pҠٯZŒlY=ZX/|[}z߾/trXe-oUXe8>zZ5#ƵO0eq]U. 9m/ԛX5.nR)h @JaN/F"dg Y* -_1 p/Agz4J^lkʗI-؂r]sSЕ"wQLH!(Hzah!PSpFr5yXI=,1VѸ/<&h>z`*p0W84Qo pV<sQɗ1zlҷOzg3x|!Lt{&i7:ЁȰp Lm>l2 &#<\?9{ z*୵(j7(vJ${ٴ¹Og_9>xe_ry5-дʂ̨tNXߏ |N:,^'g Stw W)/g!فƙz cWl|1r> -[ͲՒ|6Ca+lc1d۳:cKCbc~ vޠ%DR~ٝ;{ePi JeM .=MVQ^L/߫ͅĖu~= Ŗ141$(r\ t˻dU"A@2i VA)4pdf,DK Y'{FY 8~%J\]wGb t@& ̇rc܌T,>8D7&/#ZTC "0֚,@nC55Hrr78@20+(`g# @J2D54?G-0%q)$ <2[($;%#K#}e%GI P|pmA@ҮLCK2cE&ǡT@@^f1fЊ%%'Md5֘a=]*v0ND - qDϯ,,1* ~~bI:c-;^Nb]a+$Mr`!x@eD_KEv,<7uUeّRdi;-:w*[c jct{v:Un&>0I`. -t-.Y5!A& xf CǙ*/gc_N+5_|t٧t.d=) KIKHm{*{\:Ȱ̜^Cҷfhd(fnQ@6~:ޚ /xȊwxͣ|KΙNr;>.@Ne\ Ѱz]T:0+ZPdsIH<<& :{ @17 Ֆs.Iy%_558"gIŋdč1^P &8CG F޼OӾxrR~ڙ<~uJ -EۄY~CqMU/_OȎAMLIo>A㙺T枀66 (`5]hr҇o1$azҤfҦЩ{   g9fgK^y,r̴HI{J/(k9GAؘ#J(1̘bu -pPŀxXbjV"b.Mds|0 x&ynHCGX?` |vWJC -\WHJA]Qy+OB\a;5~\ܔp8SuWT)K t+)uʶbEr[9JFZɩ -),QŊoH/(pʊW ൊ`0֓mHNYDì[ҫIӫ]]O\@q+TE/VA+|>EaeU{$7U񆨇*ȹĢè -5*.q-T7@T/9]Sj._RwMB p2L)FK< 9* - 4fQ^F@}HCIM(ޒA:sJOٟ(|B]Vܓ)k<ѤQ^j'4S:Yגӡ'GRjvo6>֔r3rc=2ݐFb1cK`bߨupdX4$6Sbъ( _LmW?e"`pE1.@|+H^4bO>*a20rq# $ -  ,9`tksb fX0! -`i' -`ـ0ȃZ(DH >wAtȧS`ِ @ 3+ߛVcMq";H2jPN[Е⠰$,}K u}gF/8|o@o?:R3}T{ 78{Wǘ|Vꎯ闱Ӛ\`R@klD^($;uέ6Z#Byq -LHz1.P^׍jD4[DcL:m^%}y8Es\ryS^UiS}'*lbt>~rGTS50c_;h ' hܰ7Ӣ3ˠyaޞL:$bOY{չ/c: 9&TBK/_F4@q_(WAt {GS __lS1lV10u5&@ɣ%v7\ׅèI%4E38Kﺠ*umkNnỎu !h1[ .zBs>L-b؇ue7ƒR1q~=&Џ嵺nn:@2T 3z,lϔR'ZGSڋ¨tsh=Q=>!AQ2/)B@u-Gyb]ÄӄWe:}ƍM,8a|wN1F< +pt-⚐WlG -U,׸.Tt\̵ xD.w>ٌJI/:6eej#=A~Ŀ$xgpI7*XCSEO)/0Tj]cё W  -:HsThCP9e/qUH$8QO_?~)D5q9GFV5k6$sg8J)" ˑW4Dbt\S'd< y3CN %f*{ʰadS02޷sWp  'L9r>hje4AA+ϲ܏ߝ76;wS-Ek8[J?ܻӲ驼'Q(/k**FQ~&_JO?CKwyAE):Tv9oMR;rKrzP3gSY+s5 xXl~^̗ߥwp*9sŒAo=Xx2 -tPh] -q@ hL"4kߺFƾq|PTX5z0T0դ ֙JtYʈF)޴|3хkPtcxcЦЕ7%1~tqsjEBՆBڼ>zCi< [4s~GeW&6=i#N",+2Vl5i.ub#NՔq'&,_MJl^y@.5goj9U0PCf^ s˾ -HKL.1ߡN[6b*7R6;Nq0b1UG:| q?3[I|̭r/j,iL񣒜=odӼD>FlOqz7? *ܹ36W]JԄ/0W8 -Z;Ix=lC茭%D3+65l;"=QQcYox\<Z7$yI|I=9wNЄ0^յs9^:UoI8b5cf~yw{#@"XaRkvIV ctƺJ+^_EiB}нDNpGrX+'q N %}M]'ʁE8x OWGC NCu7WB~E>28iO\RPl~B]J8ң dQD Tآ|@p,OF -\#%Y=-;M!gEO yv5 1n-:жݷpT&Z7.6tXxR$)#+0RVB/4GFBOS8_?q*=jL -endstream endobj 15 0 obj <>stream -]"0W{WdǛK<=.4,N'dr -3Yᣬ.|5f_\nM{914#9r#)$ E !PcMnaN[JrS31wB 4Lf@3z1$0"ΏͳQ>)xHS_o~޻ɜ͊334`fkC;]tS-<] M!ϳ L5>A,MwA%wl߂ VRIe_K-檝{uEj-+K13>;)zV5toNy9d+\dxCۂA0viu?*'ۻX(N242{Q}KsHy',YyvI(q3eQAz./|H1d&`L7yّ '^OUvl^=#J6h{!)=>5֔^+8ɻk՜@~^خlح4~{bc$ g3:p#pYzlbmW$yZ s@WvpO&E@oဝ{~ÍΫ)|6m:kR9aY7;hVo/cؖn3g TYFF&Lj,41.an& -Y B5:/Y]|QI=ₙF9Klַw5|NSY(\j:N>2奋g&E:9QX($vrc:P^H@QCҨ8K$qIPd4#}oʼTD~f(C du 膚PDUɩ 8MNI(<:٪'oz3~寇(mX!IVN!Ka$]H(KqBP7>JE`R(4zէ5-@~ :p%RP+uVՊ0p36ݏߪ/JυI.y's eZF ZVgbKvBw,@R6nEC@`ƤqŧB Qŕ UnC4w~~/ 4h#w _iWOZO_Փ"nsz{F>n+i>a| p!ӕ'm2i~ -$2uǍ.ȍ,Q/?IH-'[[V˛]y}p;IhyW_ŗQ+~\ p7kuHup%agE%#4c"[p+Oqo b @d7rߐH#}frW^qXG>c"f<G|r0MlmZA:([՞RX>q(bTvյ/]'6b8?HY9CёGS7sPHenb R_I.3#AO.ԩBsO42\np`֙_e}J#)^ i!JDME_)yn_,\j["΋T4R†7*^Lw9(7ddžj):mmcv HA뾅 uʌg61= ,OxkTw1c(CmtܠCB9Rp4W(m % mqC t,D~<;*uhr^cQ!}W0ҁs0aw^TѢ[1@=y"y`U,0w)đ< fY%ƪ3vd!mŒ,_{.;_LVDnF5 vhw^HRj{zՌ l 91dYOjfuRDiרFsl,wf֜/Yfd -2H! -LHgL%hIRH"m‹i*>5Jig00~zgU頵+MuWa:rZIrO\7d$RK3R{i"$y'Z_fٍ4/$\G *v^&P?ܲJo'4dڬkZ@27ȡ|[t`̣8+8W~q=^8A ui SzTUuOy7rw? wD%©ᅴ<\qQfa0S}hX3`ӛ"sDgD~+cU\ +c`pZy91GUD`[!TZai,[qLv}=q1R^鋍EgD@Gey8mBDx -lYw=uQ报9ҺTOJ - -I@Ht1tEE9ȼx BrF*_|P@I((C,9:n7^9#\?Fvv Y +v[ErUkK N|0'o;A -z,5`~h(R2jT$u*i>^ \{#LGA0mlUOt:;Zww%M뇍iu -*NEplU -4,ge:LLfx -3tTwo=]]6u*T4LaʨQts*`3;)Ɗ1]:" `,^6D/큱XB 4xuS{@wمezosTD<]![VKe*$xlEdŀ3^JK$|K-;:bQDwy.e{-CI( SYri^Z(٢"nw9T2}[A8޵^e4qq*3݄YVԫ^X+{xjt ^)l8~<72V{c{jmkm_`WNR{ݢ 5o3].cNlyS:Gj -[W/3@El5$QAR)Sƀ= #.^'q£w]A|14)pi]CKO?O_M2Ov~++ &;~Ep}eN&_ vS$GgG)P@ -NJLAF 5NvmW_OOz?: -D!nbI27ھ`F$V?E{+iq^~=͗T/),wn_|lu)}#؞E+kڇ5JU5&<۷7%@L>c{&bh1?3ruCv;KؼBgEh=Pp]{{x ﱑaWd/$e\Z?N!m"`})H"ALw۫ExH Ey}yOh1Afod]Z!͎kuuZ/8%aW -a"DlJn}er'rbJiMDVڢ98XXKI_}`\*B1wt0Tٺ ¡mP\(m +!cr6()yo. -ɁƸQ ((JYUHL ~7u ɥRE(ҝdRgC\ mxk+poLnep4E&@!).MXM|*hfJ #U^ -p^ X`[i&ep8*ƎyϏ>"3y4*ɜ'xu&-SrPndg$=CD&`:,z|$]'#Z:ܦb6+\ihE)maMc`{[ -HGw* @$!wϙ$=i [ #xW/˲k8iU -?$"T/*2ɷYBngȈdU --Q)b-gC8Hfk捷H88Z輸 XL Jh\Ie7|&9GEa֠;+%lWײʅɵVAt|`,q\̒i/8Aj:YUіoOo"DM:͸vjitƉnj+ xK`W30Φ4\%!H'5SbL%_:\YpHG^T/_kxg`*%ՌUk c%{G0iJUiXrL" -mPљ! 8h~YEAx#ۅl`g?`dSh -6FP1t2 -J^*?uN))5q=y談c;ݡ1ZSr?d8KzE @f숹zAG'gZYcrU:D0HYhte1[ d *a6CRxfˋT-\O`IqeK6 - T^H&0wAcPuO|8pܠPM/hOy5Mb(-=AwSH8J}cfWZ1(0ppqyDܦ%i-6LT1vZ89C8In ;"$ -wF7{B4ouYRkQvX[;,k;| )p0o;!Eg 墟 -a+UQ(Q}R'|nUVֽ==*a-sl!\ -^4WEYY.nR -W@ -{=_E⮊euU Ǭ";=t8hhO7\)~"j0/TB5OpM*,|s?RQ.!#Bj$ {ѓ(^,Nct[b},}!(_8x71Sm5vzAbytU6<nXzٵgU^/x/CNVk/E Y,j E\cK<(Fdvh5MhE 3A$# -{`1ad0caK!؞ -T~߃ PGҗr\P ;$Uq}Xtl`4AU `c![-ܩGZhUd.pܮ1b"[+veRE}bH *L-Mtz۽=*dOurꁷG,s1}&^E lJ5V["ɻVFYi|ډ8*S4$7syV36 -A;_IX ]᪤s_#*V)a[csu.< Z(rcT#+#?RY u]$;ńiXo]wۙ -5>@Nݘ"$>ż} 3R9iqjyS% 'Fĸ?K -ʊͶ/^Lx/R="Ԙ/.gx\T<\0#*R -.(O׬:& ImlWM`.Û+r[4 BӘ" -CE.AW^<ø=1{py1bpgcEm /I166nb ٔ{V(yJy tásC6q=EȭZUj8ыz1 U7`]҉o< 11J7=ҁ򆿿( -Yl{ l.7nz6Z):圥5H]ET[ZVp3He(N6ŋdʢp_) :2LLN<#zh>#OňrŐ]L'F23!1{Rȗ&kI%kZ!>l֓Ӂ%'كr@vh>܄!nQKFѣEXEkċ!i<Ц%& Y(E-H=:_32tːi -WTBΚD3/M3)G25_{8*TeP[TxZJπw}ig}Ţ@OpK:1K>ʕLYY^u"IQyڧ2+E(Nwí?1$X5p8{hSQu Q⹱Tn]:ME84'$5`=5G-|%g'X5\zdgPPЀFSDҒ Lp,5Ffd %¸?Jc&t:YD+XcBIOP2>Y2,6oN|0Sr89KOڀ^Mg0}V0#AB NU>I^%Vn7Kev;<.KB[?"=So%|7vet *a A]dyZTISiX)Ə 5b$t H{ ȣh]_MVl_ '9'gtПB'7kKt`ӣApf?$uQh!>&khUfGEWͺQT,PoV1$bqFs7v 4rCwФ,Rx &g-{f$l +hUwdȾ/+@UpbTeGrĖ~HV&{ʧyHVّ$ȁ'iT>1Yb>X+ݑ5nshΰYWmT̈́nd?˖_H*ȍr%Ap#kᴴHzͧbWJDݐ_Bv;3V87N^11FJ5y|f2kmY%DI "(ow*mhǐBofeLO6sYjՏ˷wBg-mT7Y"E¶R[VUk?4N2l4>U \mTw̓:زS,A7ՍC \/=,]AFL漙bެ4oe<O-lC2lNR}x4~tA TT%De+솶t ;kVS!Gt"\BXH5m x9+-WӰNĉa[T~wGP`l"n_곆ܪcRSt -?+6':'>H:aߓ?ҳ5uǛ# 0h'De\Wi1 3VqS3K'= jJX8S =R*馼G 3c -؊A4~y`lj]lsP~ U dQiVV`EMw 5BU17 fP?}0׫u7KT{ 6Til†C I{ -W_s7lXe]a;ֲ傡ǘX~8V$nR@70o8<G1Lbhu_D|f `-g@~SWxIHy0&IV<,⁝(VǏFg'lYw 0!(Z!FYb`@m0ҠʨBV$Sc- $\F'1X7i"KK`I,TL~g6e'q"8IZ0+`,GG9T|=ͨM_w("pye -27˖4t 9odH)OJ,ik2Eqa<1*('ldlMAu*MPn]IXR b]뮵L(NrS$5=,RG$" -rj"!eJd]%wAሟ#E~ J3 hr~+-H8ã:*hɓֆxgi-B#'YTwjn.M@˗ '^%f'҇Y8qW^.C_POI<^EɠMv[!0Ӓb0_ڲU(&<|*۪(1̭͓*$%.=HqBV<%vsVzPBb S@R9EU {6~eh_NSyQU.aW*I͎d;5Nxb2i/gQb}D"Cib -[{s080 RӞy0+D_N͠NpPP\Nl<з'HnPt_ -?G`g$tuD=P?Rlb6Sg[z lpaů4LVZX -H:"8Y -~cZN)ɴY4G&E+ -j"`!]G!Dmk.uaUA#N9{I٦kҐc돴 fDE tǎMHZ5jq>p#/Jk>rN/v("$$7+CVH'f"/q"!^pt,٫9HCkȳ rz.u{gK c'}X# - -W!Z*¡\"Y*ͽ3r) '6G~rRO$CՃ?G![bǃOOk0ɣb{..Z@F@ ݙgBmmlF7n;CPء&n8"L4[/XGgIEQ+*wigQyq8ENS1!! N$<0MNzVZƏqn%/ LݺsUVQP -*:h 7 VOC>/g -otb}.&G8U0__s{Pd:N+El[뺮뺮먊(((8ZE``5*:s9;;Hqjor%9?Y8Zb)uL30a .A,JZ.3~vW cczQ;zxNJ y335͒?+ c%8R%)-:+OdF0LV2T`9dp3Vb*x'[ՙ`lLBEP~ح;=s3R\4NZ(l^i&4m3U$I _(.{ƣ)D%)V`(;FʼnƖ.gjOrhC(~u&avX{R3VnԘX"CVFCբJL;@< Eo  (t8lJ;SK'1ℕ%VdDs=򠂅%'fOg\7Z/eb ̶^YL6E(e{qɠGUTNGo"0CkI(A/;Brt?*پbXY>R3 4`^lnXR,.;=P&sTN2R\;^f)*ض7^g ϙ1EJ-Nrɵ0A|6v-<+(t&-.&*,.=͜ixq%Ɗ E+-N,w6fUin| %-GSx'lq'$%vbqb;(AsISX~0QؙZ^IJLOQ2T]9˫gzy-E*N4Zj33ʉFɆʉ -%p4&IdOTJMqkfGqIOPi%@i(QfQ=n%fbFW` ]&PpޑDGh,I]uiWML$ǸݾఔJih5+GQ-z+._fz.ɫ$wA}cU3+dÛzU*%`sSmp+t9`d'nJFa'ydI!'TO*vŘT ǤuRX - u:E~PrJggyڢ&/5- \FKK¢cdxĪ=عbfJVI7==Ռ-hMef;[PL7[tXӘh/Skn| -4",ܐ??,!dT$>~Eu'pXS7S\ǹlps5]Z϶W 1I8^l*gOLlJui3KP۞K]of3,VDI4x&Y]S{yi{ST2qjpJ+:1u% -&v֧wEvf}g[36S _i'(gZs۪ɕ,\RWkҢMPKU(F=U#|Xux-ӗ]U -#'7;]-155^EnUrk9C$L?iyBWA/+6}~f0WGY|FS-gGy9Ÿ$I ]ؒGY}5 Un ZKCM~%VdS 1v%(X2["%f'Gޑ ULM n53-^J'~xͦ GSP0j^`*V3rULRiٶ}e$ xgMJi./_25ڌ j.,ozڞhϮfrway[b%DMU tTF00<,zIJ KpjM ''} I=~vtӓT}OLqK&4BPH=&N -:W 摆=EW׿uCS]('=9)Aͫtu"_k) Պ}A6BLbUQ, -yA&SzOlTRoDP?+cEd>rh4WW Id?C6TA&jG=}9zGX,{G"1)&;(R:=v$qaastjC鬪3d$RRG|:ZhŎs yWuĊ>R\9Sb0Ző$ia G1*w䧝U lgG4(B 8zܞz)wz$yG%sdp~OOE6,pκd3&χiˢv/|cMWY_=róJ}r?,fG?vbC/%%v|գJohܮԐNrr3C"U0(Z+Z?=4)Hhؐ>e/;%I\tj8rЀpC i%!r< |$MOТ8zJǔr":YWIy҂>ZvX5TbGnrKlOPnr~ԃģ `S?Gw1V gz[賣b% = -է>mPyD~~Z3j?[b^qCώ_{bKO b "g5,*y#KSL5^uԫ"e$紆NN -rnؐMrV0Y6T?|6TRG)G#!;6ٱGn~3#4@d8A *ZUg<OыFtAOqPȇ. '8G"9? 'JFcs!V|VP'?c䑀+YA&ls yAxHrFtԆB cG^IN]T|ԟG.GEVE!! jd7GHD9KNc$2~H`R7G'!?贎srf8?$TyͻzeEK&Y@x5x-UC*~J'GNKvt>0EǤwUp$@j2I,EvBCTI? $I;`G&fWG5)dņv|]^ҫ]6!Z%&6?zФ'??}EK&OPQ C;( WD$⧣_sC"D-bK -GhCܐGM -1#ZRv44#AϪ\EDEءwA׬# -KOJ E;2 \29S:$MNigCcOEL`) `H~rzAmXQMN+XVl%'^V o;jxQQ|EHKu0)$C\yÊI^q.WwHd}ðK28rEĉE!ǃIbZ!dB&!G/;FRRd?g #cFn"QNum ;$)EdpE7yC&sD d"C`A` $3 Tn(?l(;%W?7~apxB='EV;M,@9$R c"x$N=rVphZA=M!'&;6\6]z$OOKQX/ǧ5QLrzEQN0wEz/&"6Q#vD]R 2QaovVWRbSxW\gCUĊI*+;bA*^1iY~.Yt;*zG*'eEnE٬KΫ^K)>bǟ~z#mv|Kb\3 .aZA puOGyMwMCSׂB.vxzɒ?d hC& #'9A9! -P7$2Ffʟ{! )z<@hsTCi^;iD)vH.zI !c"p?z(|>dSVQ.|bܦ(/CءI"<ݧL̡h#$ܑ#M]#IOD`dydtzsbI)K9Bf'V9(LSbx+"thfjh?=:jI!8B~zb4YMޖүO ޥX.eSCQءCDI$1)&=9 -5q+t,?`D-&GXŞ4翧V4&𳳒Ójk&YyzMvM*D`.SElU}u&u4fOC)U=tkE"#)~ -$T$IP? IH!QP ]m8$ -PvH7! 2"xK EF1)$@^Hx4BKAͤI~翂8)r?҇ Ȉy|_A\/V*8IL -993'Ղ硢K\/V/E%1Ap#T fҴjP`Cv)2?@R7̘~I㧣:KOR F ғxdOGu Qrv5KI DL -[PwEdQOpR.&dQffg]~5xٯWC/.;7LɸϪm&τMXP bZ1D"R_ -GcS?#KȡXe;=~d~rt_M+/?7ώUrDFJ,^vl1$&} B_CE. ȣ7AAd89$Z]O$ XN~\yL+ o[+?C|ܥԒf#&g'A-l9';,8Jr#(̀zFiDX1&<vIRZA)D VKN -KfKN>]xbE8lEXL_,&Ԏ K~K̊G?f_E\5Ҽ]rrCYQ+pmJ' FƷ`QG=u9W0ܔRr[N -O _gy;@ny'|%L(rԼbWr_J)`O]j?:J!+ӐJ.UpjGlG hDǒG^&9*K6Wb{ 26T?A6eLUȴ~PǤ|kC1fp&=H}";>b*WőU~Lb$= -J#}lߚȢ4 OoJESBV-!>_ȑՎRןf7;:Rţ0d7=/mN[z͢ m MOAOS̊Ej`ɩ~xUM^'IoS1jbI=5e~\o~Lw]vu;*G^Pr@I鷾͕\}6~ -rG$~W²u|zI6Yȣ Vɦü'{KV Ռ~VԂG=^k.6KEO}a!kR7""(gD)Lk >I`p6A+Cդr$7IuԊR*ѦpGP$/Gd٣wBȐ$G oȄڰGE,Ȅ+A vҎ-v|uĂi(?A\kSW=~VujZ%{J_YRC9 iQd?T膣/<$hE/6֏`0?A5yI!s(jGrE(6דQGrV/)/dba$|:W_ExbA&j GC5ի,&T2?-{noD a a/CG!:2 -Sث 92`0eX<":O -&]٧6yzI8I?%T!~>֏Q$IpC"P~vTI $~KP˩`ڏndϊL, t)#KI 2 }QFOA-dOWT/?;+.'&OEd@;#t3#'>0ϐ8#Ci^ZmXъܐ:pVg,$m`i I2bG"iDŽ)YV)%备It$Ɏ6O+EeOCٙc $X(ŊRՆZ35+8PXA7PNvXP 9MOIx4t+S8$0a`hsf|q-n Kzzx4cEfyݻ]hGud$iեΗydߝIzd`SڮL OM(;<-CKv1l+K }${&;uH" ܐ7=I}$3 M~N.㞒ޑ;P=#T !pVfϏJi(Cvӳr b6dZGxali69 -rңzJ̤|׫ NzZ1YXc.;|~jQA,9^*_XM䰤~ BRxK}%٧DQWC8tѤr^5+4{E%YuӯU[E,ȉV_v`7+*"'??'y *f&W0xRj-/W-if-*sjjI0]$PsU:C!(dG?=b /m+?kjFutzœ 퇟e3􀝠s@G@<:BQ &PI,KQTy=H x' GS4AS)hD> 4ː("x":iiI2CMc$ʲRDx(n+96Rx8,K#L,KxxWPyhA8Cpˏ&x@$AX"4ːv#ƣA=HL!~%yX#9F2,A<JqA,KPJQ'H$ң$yJ7Yfޯ*AM4KRD%9"$ÓtQp GQ8#i!(iyJQGQ$1)œO#E$$")$$$ -pA $!IJp>DcY(CQ$b!xPŰ `(C E`X I_if)(BQ)&x)"ɑ$IGtyI2sh IBR(0 OthIb9r,Ia("#x!R%)hfI"4 A)2ODhF$2M4KPdy&8Na7N'ߖ'U&ߒ(zY6S|؉Ѳc鰮dU/9J2a7EGfU2DȋB[hu%+dͧ}tbb;ɧfKȡv%y,\8"cgQ0~U4"09%HMzUD? $ 㡓ڭ읉ޡl 1)1Tv+J^5Zj2V Xh(Jd9l3/]5!^~^Hܧ;Knzj-~#&}d՛?tV/[QQ 61TMTǣhv(Z(+6ܪ]tGC_k##08RvF7^Y9+]vLVZ@P1 -K0NT3ҋXbbd&A*OK"a?X|e(G16Ic g_Eva+'7A'~!](T;$VTrpX^j`ygw08bX -]wu$8zx*f wUEհ=9hIbK 27.46Na*JOɨwqS ڣg*%(;&N4moW^{͗s+lJdC16K8սXòGI+t%{UIXV\fWW6AvyV\6`͔'701S–!eb*WsHѭhܪQ؝PtJ>Y9:f7I$YŢ -nyݰ\C%N 2䡦$ -MH_ ,ب*0 a:PD ,Rdr=MsbV+:mq*%*MbE?rhN0*:%392ŨtXdjeXy`AÃo/8^f[UcMrOQJKQI2$O??(.CvIs~e+ETx$Af mYJ%|*Zɥ>yTğ"LhC!3DYdB%&Uz5GE -A_CC&WR Nu\&F׻a.KT.Kwzu&-쌘EϏyS\ާX4Y]$QA<`jĠ&&gG.bE(1;Ud=[ Vv -jzp32N\*١jL\La6Cq~!~j5GG1I**ZEU\eCr¦Bar4-5*2*1K5l@dUefBecޮZ%N6o͙r|Tg]T*9yېwGs0.'3G9N<@iGKp8i~WER;jLf39(KFC~S2+)(WE%3%+vҊebh I\SdzI"҂B49٧V -"Wj{ -B84 񳋍e$rl:ˊIɛ윸BC$qGw4#s !%>"Ƌ)djmf²]X5šo @7IXKYfΚWTLm?aG]`k_m=5#"Sh R3= -ݔ*l h8q2+a)x~_vC&?駄XZw64tz<져bw+#-i `SApPG oE-4|R@B8Dfºw'\*>Se23}EOqkl( Q3eaIJpXkl[ncEƂK 3^=բ -W +ww@cc4WAyKpe#[]Q)CNzz4 gܿgrr*oPx%(RI+f6U;+醝Sh[nE%v<"EH<|ud c{|O;QqzbpGeP$#l:( xQ&6#eliV[qEU`^ӏ,"ͮ$SюYjXq~cp$1.r(|%Tjf8b*_I ?1ɢ9+*J*ʊ&$<:0Vxk["%efwCvwDCр엣Tt/E'h!9-Nb(v<gKLs Wg`z ^WG %g;ޚinR?JXt0ftwkU  *LR^i.U.it(ؾPcg:A)y|X`0L'(jGĵ~CdeC0ișnaK/w[^w'-{dzYLN5k$9{}: -N^ -KƊ E5/JT3=i/=8)3}Q -yCjѪMwe8[]" U0es~F[~!%P9b>Iӻ NKtvΥj?e†()7f,YĒ|dEqH2觖Y.m8e"" ֌MO$ˁEE1NZر^L*/Uu'h ]ffh6M7G\j( pnG=&-.lt,Ph}l)ZomkҪ{zVze*S^J>[Jж'lLoE,0]EXDŢ·edJ]D̐1ӓBKS*.XPvNO~evWIHDGC,ɑO$-z.ޕ\:U -e[o"谹 -FDU[\nlZh aŋm"SXݒOؽ\䔨^=Y^]wc3U+Evzn ㅆ&3YZ3 ?L?q ]v(TV I_YXS G&qUI-v\d?4S+٪N1vę孊:$9?F(VIMUΪ[(fgM+oc[4Ql_}( ʎmS;5MfTIWV]tUü?>I=_᫿28zF! `G!|,^w5#YXbE^Z҇,uNXv,g_7ˎHLrf`oeʪ[5zzǚ.W6nwťn#$ߊ`C+:çXTi>c:ΗizۢO}^p0,%SJ^Qݷ\FcvjrY$)mJFUVq U UԊJV_a{/J\8TVFvAQd\IR[T5L0̋h\j9A en13|{?v,{EvUgr}+$[#A&C4 :9#NQ^XH߂sH$v)f;c#Q^`>Gs,u?A?cqZfE5RM_(@E#&YS09mDILbb[ѕ9(iJ0FTDg=*;(CɡᕳJΚӔ\40$=IlgOh_W䨢SBU[=q=+Kh8"AFneOvvTG#'+oqX!S[}j6\ZI>yY<jM%&AiI#,{fX޶l&=dgBTtʯ$Oty;ѷvI]:M)NaYZt_]K7udlGY͏[6|JjC O;5.ۤɩOСB Vz_dީEyͼj$.X*fz\=4h*>y;  ߊ!ML~3 bRqdE#%#H9]T3+8֥^ .sIި_G3LZ PqⰤC · -W^_5W$(L`7bz*tk.K,6=y -͏j('9}E$2ᮇWRߊ -m=z\ϟ%ZGA cH4U\TGG1H"ٟeDWsHBߒX^NjQC!qHU\~nǨ4ťp}V,՚Q=#{&u;$D~κaW] k-o]"bȔhqBF S\q_L ,:IoAxTElSgT~JNOȚO'+ -qE5z?۞j]ۛt6f2k%9WbewItdū,$!Umbz{>W< -[:SJJ]u -BV3J%%F3%q.ؽBߎ6Vt>? (>OJ4R`*v+|-xYV G3ģx6Ee$E)r2r@::zholWX[ɫRKC8RHTQ* -9˪/a Ab uNz~DNG Yh?K_ɥ-#3w+ʡ0[Eaj%ڐO?6AȐ\U&8]I3Db: \V{(T)`i*oi~v)6}K+v^$JYI#YuOR*I\WhZ?eN? -׽L-~l~#e}GC1ϊ3-9)*:=&LOBvLSl! zg8OSC~wK[Zg))^v%'|WI-i!)ݦ7ҺsVR,pFI9CN~*ZDJGiwBH!$L2"'!}L}P:}DJ[x{%7Y`|pF3 $#dÿiU1өN5QPt㎂L헗dhupL'N܏3BIb#Sojcگ8>csDg )xv"S^WTllvOCqC!qV2Za%An)b1MN}jH"C`gEeX~NAk,G_L4|MX!fo&Ͻ*z?gBCxJwrbc\~[z{Ve 5kb~VQ2˯,' XG) ڝlݮxp` o6Ŏ ?"f$qZo׽pݛTrcd6Ϧf7~S*h7Vt"$*/|H~[PO\WZ*S=>Qw{S.^Kż=DhI(KQhütۦ9N;m{+XQ}-S1"&C("Zhw|YP1(2'vmU2>CC~[򴻻TAD 6><@ V``vtdSX+Xn($aZ"4a F@((a6Pv #A2h 2 "6PFkΊYKfQj ll K@B*H -F#4A DA JЃ@ Fh4XȤXӞc#4B\&@|p+< `@)4! BXBx#@ T` 3R82sr+q+)*37U0fl@ Hx+L -VB#8p <@>` 4  5\h` &UV,^f7nA 8 '(`'B px !@ -FP ! 5,l^Xؼhi̟O^| "x (4 [n +L! -E x@1$؀6P0@:jȰh, -Zfti)T$̠+A`&` @' N 0 pPC/Pr2VUw)g @q# ^(4 ZȂ`">p!*8(pa1PNu,z(} GLn6$ t ":\ԘRMjߎ&WᬌoUtI{`\K6a2rk h`)< -R`Dp^`c̋ 0dNN`[JWr>y)e9OVrءB`H30 ~! tl"@htr@+6Wmo+g3$esј9a1t0? \ - 4hNhRh'WI{'rTJLiR tZaԀ|!?A LOax++.b5(v }wIgs4p8A E(v -qÌ)NVdZl0lLjX9ᢻL)LU35`I6X `P`h"vp1!f @z ;px&d{nV6.UfVzax-,blp -<0Ap"r0AB q@F!$drT]**wDmp(I^: -.ENt$~fWf%+֝Y>iexOXLg(xjȕ%9u*I9bGKnݩǬ5VZ(t)\4u+ٵ4wj(XbUN9X@@*8ETsXy̟9WRLrZ{;erE - ̥$bZCS;[.= -峢eݵ- -^*ѭL>XQ+qiE!5J*}(:?㕻3$#MP65֫,$Z.3R^f+_ϐ+_cxjG7]lvrZ\)OhG('* ŕv@jp}`]^< +f棨jξ IKaݩ Xtڰ6=0=}(v d)>Qպ?.)q[_9%yJHicx;ݙ-gG]h([6;a;`{K/w}ds_CmQP٥<a-zRI w1!ɍ ֫4(*p'=fa+)8DACraRN5R1Zf5\PKOq]\_gRp)zFDz)y|;wGJhT,}E +gk3/E~&YLդ({ػalOo'M`x*?7;Հ%k? WS.X! yhJo?wvbq;5K1x9 JeN"]R9X0i8؝.K )PͯWbvrw $bSPDƊUܺ՟mҲ]\+.߆::aֿ9^n/J\11ioYd| Gqc,J-v(ͺOT,b{a[%6ah%&;Պ08C#LzJ.ֻm3N`5ɢwy'U1UPVkTZު Xd{z.PL8 %FKmkwTCYTQ&U|Y_SLB_ P?̨IbDpsb•ڒKn?ͪɒYP;dr6\p056UyDI,dP^K⚩'UL{NTĢXMʴWZjNُUQ/iE'Z E5ݝ)%!+"HfPz6ՊOgI<:qwԠ) *ol,go@SrtCM/ ߙb)ߒx| ۲(x4Xj,EƳ9ޝɡfٔgd=04!XxUL`(n 4Ub f&u}y#Qrz fLdf|Mnes}z+Qk٘[Ê\*/dO&NZ޿Jlޑg`&):?eJlf,D3yWOpњ bXqn%&h-`j 2"CY͸~d5pgb]r?[~L -Kqb. -"3J(p&U#SX+L+OSN\:S}CLquN9fz*E/%STL um n&=sNgE,'h|H"Ċhn5lvحV -n2D4;Psb--`p6,NPX)ym걖7>bRXyBP"T5;[WB+U#ŵ"_dz)wIXtI. -7qZ‚glXbn?ϥrcnh^bj6Yv¢5~S[8YWČ -dVlg]d7jb'gBay*DLHMvs +3^l,5o]vV%¸U]z^? -]w< I~P(LbAK0bVVhNZ'/)'("ǍZ -$eff1ӛ*JℭZ W,WYl+xGFa` cn< -ڡl CoHш݈吹ɵHJ/&]CpҺMX5UP%N.&© P#E5O]8fp1\h%+{%V߰ԊTM? go5tD뀁tEx -6Q-{UuYÃN'r(O^%ySL03c(.OC_lv؞ր")zK KdBwJaٰ۬ 2$BW ZB7fpI V3(6<Y̍Oq/%"nESChqab  \dI`4 -=W5S֋vrdVbelRd-Tf.N^59ogVtK3Tp/Maov\\`9ElػmGjl0z]z${`8, -/E2LPld%@ԠNVs \I>eⰆ/ E/$hS*` VC˫bz]ə~@nQBKyE@עDeK|ڰEKZpXqÉͭO3MjH啷v矸'*9e2BS08`ljrbs|pCܯMX/UV|`Iǂw¡M08 FU.cKX5+OCA+fPFnrtwU\z) "cQXjv){ֳ3dLtpv^eLEwq+:wm*4^8^.p - Z8Q\t&xʼn -p 1XtZddzdI6t`-.NprCOh0 M& 49ä&x,Vx)1=L2TI,w,|%5RT6TX-JP'n׭;'Lemz2S~b |&Y)[^gUTSɭXb: -\EBWfMI - Ư0%%Bcf\ - U.(C5  %%+SYr|X!ߙ!jPqZ)x.3_Ҳmz+J uНvpɼfZWڥf&UN*l}a8P-I^զFaJ;pϓ,zvwc͘M 16jt=H:j[$fr%PYӡҒaҙ$၅ -m::YL3Z`@H)FJLZoQVsLj֝ژK&iF)U*75*,0V*.aL(;Z`3|5h^dDp1zTUX]D,ȤuPOTY8"qOǂkG1/g錓 /T^R{jC7:¿"YR3Jj~1EƷl&S"̴ݓSzLԥ%׽QH% -^W?\(8#wCdPA!:P - -X? !I [ZzKiL1FijAn~l:!(dMMq*l: 'Ovf˖Nr -iJ~ ݊BGFq< RYa:lFq{ݝq_ADb^чrn+.߂41={Z,v: JNbu}m~|eA7Y1.?s[J&Q&OT4ƫ1Loyu"ŊZ ,hM7y&wezv{70bWQrJYŶR;6_Nc}R Һo; Q$P Jy,9V*|(ΏQi! k겒_rGqh"E/eR@M0)0\Ʒ:-R$r泮RӁſ3> -H$i/?::SUQu\z}Sʢ䄄wEQYEWO8b'WG(f!;#Ώ nSMG9տ[hzokC&Jǝ/~;u%J6[yjA+.I?Q\zT9íMɦͯctC%-~[4 ܦ.lxGK6WYZ#㱆"?nM/ꯤsv}d-?W]Z8 /15)?ȑAɭx`GG\18&ɇ - dO37\.1lD6`CF ɮ_b&)1 TLr/%F ͚ef.^E:R4|WFJRT)f"ŜȨXxb:VumIΈ3nΔ~׷ˏS=Ú_:^*eAANyh~]ttfjK⼡g>cs\i|QTs\**|0&;*|H;vt7^urj[iu.zme-"4/Lp),R^0sC\tzbʪ Y̛# -z83[t_R4xBܧh-&ˏo41WSãn.&-)XZl^iIVSh5[t(6ROe_O5窺oXVj_yov fY{=XaaCeZ_&In= Wff+-߶͚'] wZ&.:7bt, -`=`|0AkFobMZz\+39BfUmW%{P^ˊ] 7p01XG`jeܕ˭vKіe{wµP\L`P^p `>hnRT\XJjoRe-;#`gZ ̅ -"bz@ Tp2 `Z.e*w}&I^QkyXK`Qx A @ (H JB@ DPSղt{i>)ԯG^[B' b|`'p 2%8Cb>< B:6ͥV ek,zf:A!/ PP&A dZ28Z-0X+-dV V,oOZvq6\.EO.:@HX h`C"?`^[qENkYwW}R)1?~a528B4(Ԡب -V4@B+3̫ڡ*QnUs~a7PJ%pp$Xapp t #x(L,0`OVP[IWKWM 0`9  ApXAbt@:@A@"p pt0&p,pxa,v ecR&8R#4p @l0 h -` ?3IU Y2mE 0," f @D ` zp/ ` PA |PvXCC053XX'A*+Az -E 5t@. 2FPE`c'w wn:yMUŎ =p-B n8 |A lL7&0p/{:gzW)UA4"0  %3#v=[#%H< gzBOԟV.TAI @!A _ -vFpBd8AA M]UӴĻr%=. 8f`Ch Ā!`a9.0"R`@LeO9byK "r<0 &(=T*ATpB*.t/t0Iʱ8jyhP6,Vb̜lXT !\PEcg=I-5W"4[4@0nP"@VA1z+nTf^)1,o-Leԃa7>255e\%,hС@< `%PPtJ8"*9~maj[fcU&)>7=uWj1_߿]iY *?gN+-u9ac ܮkt5݋ D'_Ŷ4uUH -Z]K벊,:sRL/*I^*r0dPxvV%ן禍0v>tힿ͖T^1h.\~O&E~W'^/Ὁ/7iG!l Y*^-(LRX1Ej&UI?ئQ,Y9m׸ch*Vi~3r@:{ ?BLLЀ8,z% h^[d4Ǵ9s8S.C}=SR4**;__3 &9H҇c[&MRL~';Nհ<~裄$#":h5eW_Tnh[\)d۬^IQ6&U5;VIR?8陥RZ%vu41ъIv FwvW?&OT3-a..%V, -o_<$cQI O~[/*fEC;VAkd;k.*,o|coNCNS5K2a^e2$!{%Xb ^V%rz>oc(X/Z=%y^vzyF# 2'<a|-]ӓ4 -j_Βݓ_˜5Yym$긆HFJI_~(}nWrܬ*<%,)K~fc|}"m~7bɖT{ ,uJOf.nu:j^Z/ s<"?#h%PTKtu Qyߩz-k<]b} ѭdt^NQZ_9}v]QWy&~W Zגk~,!Vr4 .el3'ECYOf>ikke8~>G/ wO+$ya(>}c&+XzSriinaRf osfMX%W[ .Yv1VOlȟ")(]ot4%ӵ\#VL>G9JlLn>\|+6V^SWL^0_6]K[6kM㴢642d1chWl4'4;m%U-oJ1kfQqZ\Z;-hyJ:BwxAd]Y0[F -˥-*4kȳ!Z[+[iq_lJYYڻ%Sc6$)> Bp@2~vb;TՂpq$O @`yCTe(Ɓ /pP7Xb:\`+t<E+Xr,9٪[[kh?̋(~HdE6.IFm)vO\K폱XRSD vS(.V؈١tMIJf|Q__>]yk b:-*2B]jɝȥd<{dv$sj=ѨIFg{=%h6UR-) - EY7L5JÑC pXPYl&`Ly44A 00I U]lb8;$jp+ @ A4S#%CzKURW -(Éެƃ,Vvlr~gG?:N48lPeȞ1 -,kNdA̘!4Wt+>Ȉ9dP:eVah E -fJnjFq8Q7QsiW^} rp1sd_SuSRsPQR^pٔj(AN'<ǺUn)K3~FnU}Y5ɦl씤[اH52KXyLwQ"R{{L& wCȡtWe)Z I bؽee%y,Ph1n3~$Hq%ydTjC[H0aPyxw-D.%؝ZR46ӬWG&uڮQ0pG <9$Ў đC; -iC$Gâ$#OFC+KO.;-iVťm_ebx +"3xVYvP~~"Ԋ+6`n)( 4:_&S:+-p[n"[ѸfѤ@y` -wnnIȤ@9 ټ@.ՕԓQ,4<h)t!C 'Pce&w:Q2Vdb8^ĺS= Z׃ 7S,'Rȁ @6` *Qc rp1^)*62<Bmx,}z+k)[uڴ E8T-|=qTCݩ:J łcN:N^ˇmR`B8*z6>6fIHXzM35UUVN3Ԋkgi-0%V` -J2䵿uƷ+"ġgU9K ġ;::ŦjNm2r<6鬍os%dn6FsK,[u+g{eV.;K2.e`g?H"w&Z9`ph%xm&ǰ6jLr)6Y).Fl\fz[dZR:Rg̷CQ.LAw`NoTy%PÉ,IfդIn'VmTp*;:fd.Y7~/:?Jm/T6`C -Pdd9ͭD+@=`@ .d "pfبɀTdQEˉ .泴؞c=PrZEV:dn<`f'SU`5L\}n۔C)xRč+'PaF 4&.Pb244E22A!.رVGqXQy&:Bq/JV694TwIwU%8F*qn0PZ2\l(L^/)mpۏ8C4CS";39"ae/-1AJ$*`%T_ޖrݺǪEq?ˏ,Geh} )NXXq";Ki ȑb 4ɆL:25b0 )Jf(l&G^gG?x; - Z`4TZ4X^B *0LX*r*)0hZj䄈тlr)Q[<~^:|]s"ncKߖP6]te5|mGb8X9 be66Åh -2nWe)*{5t!GJ!h6(;X##N/יJ*>ȊNel4\xُb2afSW*0۔KRv^uSy @ix1*"^3H2'x-FMWknl„ b4(:Eͧ%3GT{^.|IP;AI$]ꎱpHRlX6'1ZåITQV -^+JR?d 5a*{%MV9mWZ E >.sش^<cpac_j3^L`5fn d()v*}nuy,i7WMR֗^ZnЮcLh -WS⛿`YccK@!  U$XXX5g2t"+qg?ȠNV*NVH.$t`-Xhp@`w#6v;ݻ[C%eª_kPn~X1Y64b  -O0dĆsEH<@vJTrV׻:bpA-\RX.};,P#/]fL1oQqYbC$¢B嶯mZ uhhώ1)E\Pp=XR '1C ەqS;lٍV;YaRk{}~b\zY ,RЃ4ٝpHaąC:C+:GHv"OnrR:oGR+kE0mgsׯKq"E}\ 0a)Lhp -O]p)./J  'x5\f}Ko2Ŀ$Oj_C|(.SWSםΦwØ^ša`8 -ˮ$1" \ )SZɊV0zAbal=|wUs ӑI>Sszz<‘7BjazԬW`EFƳ:b^;Ndv&RP`6dL0 \84p@.3X^-D\%9E=ޏzL|H ;-]GDqdMw[V8Nw@QA`κYlL1M5&'ph?MsPx)e0 WDˣ_I--=EErUIXЇL -əjJ_I,2 -l͎oA.<%}Pq=ܚIz$6䱂Z|K-w8!Ʀ":kbgXƎ/Ce:ˎF\ -1$q\ˇ*vtA] ~UJ(x+*;̰LT}BBK),4Wf{,<憢bn[&heM5j^ }bӷ" lͤGCegSTzxUSℕÊP d̔^p MVM.HG]Џ Dj<ÒWN-JVRK+BȘi.˫!E%=0Ac[aҩh+5۩fsUUV -FYɶR;44,P@8RxgC_ŝ)?Y.-a!( 4V/b|x(7t5,Prج@QS["E -KxZPDU3>Xr+8.NvPl0K;jJxhr„;;:2"-iM;=$|Vd OS2I$%) }|g,OYviW$ݬAU#e$;ŭz,"y p< 04S OU짏S$L7ҷK s蹀H2#%˿ Z"L\&_#7 Ԑ ʼn.8+dRl/\ʆӬ*zE KIJOV,\0_.YEFpq&+0miFm=lRK)ɆYdTZ-LV9`nJ?w:÷!,$$D r$Fjr$U):DS+~ު(ZC,,؆,J`ty},h2d,tMW(zh2[RRMdOJ(`wcj}5gQt:Ԣ'%(TlFKMER;RyCЀ8 -_a7{~JWSR?VLr& -mڢDu"ktX'lL^6s)2vK1X 6/2; h2p(bzh@q=Y@J*8Q Jl(Gә bz1ަ(,!,\BVsL 棸[##GiH# _E }lս[Rcu!Pß~e^fUIL-gNnʧ t*DY4KXk`ߖ-[ך3f!  \䐢k&re$(ǐLP4+<Q2g"j;Mfէ8p5)EIZjWPz~WE -'?NsgG?kr, -ϻTmEL*y{I$DŽĠHg! ǟ ʼn(\I+|)$ Zㄮ7l|X#6#Cu* -1=lʪV`~zP x9͕,fZ$GG='y_G%)7_٦$uSݵuy,50aJ-==qjB{Nb;wm\ -åvǏ;9!#СTOz~ej%/)[ vb+!S؝bjJ>gt t~lk$!!qV$"*h5VyU2E~Bdol~ ]z>|ϻ Xg% -DEKKq{ZC`C3-\p)ɑ S-iG<Q1zS=YR =3 XTv gmS:p`ѹWZu>ˢ .i 9BnY?u3s[N5pp$5dֻ|ZV^TC ̘ -/o (hid}`0 M9$ަԒPNʮ)l&Sh)! )u@#EJ گfk @4C+MEJLW5̈́ǡgCG1EjXVlOa\ 3Ez3? -VHpJ02D%vgX_ d'ڝHdBC" -y]͢uiÐ)YS>I;;ٞ Fb-Ѓ!'R)R^rӼҥ*8:jh;'IA1 v׏Tg^[wW;T^yJr(" Tp9񶿖4Ie -&d#1tpvTqvІC - OI쿁ec6Yn+(RC#+f䎾$D汣A-~VJX; -kF nŕy`C [}(x!jAIoE՝ȥd %zy"3#2%ZRozVK).E 1LqNgOX lD 3Zl!hĜء`9{ A2B"QRD DrWL'u+(X8V{5 !K6[CT+2# jVtR=w4a\9.U.I?=:*1ѳ[Gz1_pגU9y?68M8T[CYiA%Tԧ"D.Itj#aVT -*U|ҢnhRVAmzP]Nj&|==! cZu < _K`6y%V&3Vqrz)6EpNe¿-VG-jI:ܼlUȴ~Ng;^ablvs׍^/1 d\7$?IlS+nN7w*CŖv^=_xCAؔ`jhťn4ɋ ~ jA -hˋϕ|S GTGE/v4t]'HhA&rMٝ&OЫ>w%g!WԂ#z<~&&urSKjy8>MPLםc&"VQ>=g>П^x%);K$n}#KԪVbwŒ*>[q3tkaF9*+zd'90*>yTԢqiǔ+*$%S\ay 2!FokW;;JɤDl5~#Bjl.O-H(~S1*&b[,O[Ն`Ijg O[zōNcpa7A%%w9LKb g_3:FT˔\p3=+ҝ}HT-/{J˓J=vPZ7 ׳cJAvD:SL+/uPbCa~+WЊo=Koɛ'ǯ(zW^UR G_2HEuܳ jU()9..S2+25 XL7pVٕT׷'f.,1ŷ<*tUUA-Y,"X0!W`U͢U08 贅/iCuj"EO?LgYΦ^g:RdMgc^v̋ni1<-#z$U;nggjG)"؏&mŠkf*~ rQSKNHWihKOi$9 - -6WU;Gj&[ FMwkb</&(e6Uʧ")!BJ'Vc~N8A1%<̏:jwSN} bRz^%!2] -OQrj{v"_fU joeKIio_}+%&~"؝tzG&6] ~"l&g~:$Em5pn_K糴lI7яp``20۶5Da/\j6w~\UN2j|5Y ?]u`H= g,XO} +o;¢]VtP'w䧢x>$ -ffJdLAdV2Yd _ⷧ6dɉ6 an~ 52: bETL -ӑ F!ib|K& ]¸,R>(E3{#OwQ0z~V(7\cLhv=U\H^v^v]~k.]yrC>F9@XZ1UՊro~UĊXm*EUu??ߊ -!2"%5؝jXaalE.K8B$2b;;:Bb6SL=zTpg鯢^ Gh y %Uɗ,Xnd$Dr%X0c 366=A'ĕ`*ZtPtNJ]ɢ-WKo[$VT6o[zuD"G(@/At>KZmppv/{fvWA84I)+$r50W WGTYT\#hC,] ((-v?O;~HlV:,efԢ`Pe(a}g ؾW) p13e?+:#4=kώt"Y1-&v#Oξ씶4}E9_ުpԧvƅlS~ԫ!Q j^G: vVEfgr`Q-vZG^pR%vj8ynx~6\?ct~22QnX$"HJ,x+HB~ hxk˪vIPNh $)mK6Ok@nE\ *!!ةEN=v|v[S$)+23zx?;հE֢6quǨ=G?A1IGV^:jF{twܚEnSV(8+6Sί$t&+:]ÏZ]~`0䖧/,E5ϴjǎ!^ W$,8D -endstream endobj 16 0 obj <>stream -  -.A-ٜgI _^fR0:敧6=(/u*ٔF13ŭTBAݪ* gc+OI3]DzZdoLW)I!2(+*Yhʊ~y2<}'I*zzzf:}t5wWE-\c)"hs5=W|g<~bc!SL|ť.?$~z? Q ?O'0EOfԛ+nF38Fҏߎ:o>+h0$V>(NH, -yAWN -,oeE$~BHb}VMN8 - -թ%ݑ4m= 7ҧ-_wTTl)'EPn0Y 4 bȨd&۬Qf)dc^QHnm#&}Q6eYU\dǏ;%hR%lqvI!cvz'eͷѡjl~ Nͧ~)8\Ѧw**wAyU5KgQQ\&?]1JP߂A (+>=8i况;e!-2>nfGIا=tSě&%}`UҫF_ْK֌-}( f.AH%qZn -u;~zIVW^'XbG/IZZwIvզK&ePJn3ƕd[l2dE -QʪnY9=l cntt"urkXż[^ ! dwØLn&ƫ,+HB[Lk6I^5xbb]ZsJ*5{V,]r lHlOQL.\0绯,*U " ~P-)7~'%ٻ> 'Mm(EEin,-\ǫ0zI74!6e|۳#m u5DI8FYxz_]rӿ`̇k%[`| .Q'e٫kV}WKJrEEzҤD‚kfKKvivHrW#=X^i(M\sϗ*=)M~dSݮǐϊxL[Ɏ=vZz܁l:[n$2J C7 /cwZI3یÑ=9$]JJ&}7Q v^ih/I`V_UT䰡Lݧ: SKey"M;5ҊƷ:>.W8׻IB3WRpJ)6s6tJOGI 7[T3Om$+8)~fwa^Aզ;/IP^Mݮ^IMA|*&VKoѧ=ԺP׳30d~W*^I.-{íԙ^r?Aj -LϝfPLWaϏKse'[oC}Oy@JR Ѹ{G NJnǣilrCC@Ѩ RdxIUmr_yrE9i.JwO3Kpo?xo] Uml&XIDŽQ`VKDq{j;uǻ -6QlF|Wj[ f5j2Fv&Ȅ~~j¦1./9[V\P¦ǂw3Y,;GR``^I~E; -J$ƿ+l!26ZiԫZ+)In8Zcr%&5!;By4u#Cg% l>_k#Y$+^PhG.IPJINmhS!X$\T)DX4}E#T+IXNzBHLo[}٧6Oק9xԙj[yW]9 *"ޞ\E)*ZBlvSuˊFQHiP,*U\fyKl䦤b&,lY4쿎D\q6d&5Ϗfp'RJX~φ4>/\v9Sfr[#ŕC -^ .̯!J>SJ+*xFi oI/<ٱ^OewbA]YJYC -*VQ]Vtʖx0,ZE(0gKQ’[\KlFqgh%NVed6E9..F׾nӻEYSrrqhy5E - ^Ke5*QF󷠘CKƉJ}vh:ʢw=]tTv -$M?24BRĒQYޢ$#ZYbKZWI4H!X ^dN#Xf򶫹]dK&imoy_%ౖx9vɑKϡl/Һٕlק Shku+ceƪϙ.WqնMM~t 4+'':0FY0&A9ߎիLO.WVu̬d3mݪۉwsʢcZu - -~q*a;McpLnOYX.Oc9EC_33fM^R*_f -Vt',ۧ;qdwkku>?LvE9&'v'ل滛\`C54P^-wU%)K+6\lx)fm'zfag,p\ 2,he;#UXLGaڢMS !^h8tru%V2̕ZSboi`9IV%XDzDԏsԼ";(TBC|E|f|ܚU< $ QY|ikWZb?{H t7GVIHGC]0ť0蘂@X\)v Lgiq+99$d,zfƷءNT5ϊ\j/}$2͕U\Z3~UsU픘Ðܾ]zW%efsGRPq [br4ǫMYQ&E:LWST3ˊ,J ,2;q" ])GˋJƊ+難ԒLo<<opPő j(1/4bswG52ja3pFK-Nr/n&CdhJ9H愅Ikf񮎂z?[CK -lfi孌q逸E2Z׷Zo5}]RM?ME´}SzG,IdMkOE$5JG*^uXI,g&;a(I6VJ&uÚI),D%Uݢub)U>{w:d`^8o]sxzI?; Jlb lf w:f֗ZRL΁&jѲCZDaeR^ҋ~a^8XV%^y&7VRejrWׯ᤮N>c{,z wQm^\w -/A -  -릩Aφ),|F -섭;ZnW'=??,JX6bj(z?e0ՐQQGE|6ibf{I]ˮ4iJnz #OR6Oq -_[h'ZI>Q44tX%;*ӑMvt=˟i9pxl<(x撇/Ϗ-/]V4NgTĚ+%g关عV-JC:LY)/|Oh(WMPo0ɭd뻢e;C9pW[XL `S+:]l(wyD:nxUwOPˌo.m0Åڍ -U5o[٢Kڮ8^cʫDg,-7i3%*EtvE5Z^ϚLfֳ-) eC34ܐ)C (VV)'Ece;$yA&h8CƄggpd7?|{O)+%'f5Xp"'z1 *NbX=mɠ%عrYaeOc`~mPAY\ɢ (Pp^^G]\*va'`^`  8I+WY\,aj&_b+Vdha*STuMk c%E"+XD`Oin'WA} SnJFNJPjF_u+#aZ\xOeY}v*J~.YP(nL7_K-/P@24ut;4]얱%wC:XX5hd|ͭbw™`N3FY3XK\bzg9Z%vQV>SJ)(~:IrRG<:JMU[L^.p)mChNz`S;.A=3kj&kh?v׆',Vd.l\4s=!Ynw2 ST\ IF_#g2OZݵ* -MF ,0de'/ ݑς\sXrwoĔN;x 3v3]m-1LRar %¤VUrA)zd:83@e lr63fp:B"R^f&8A4UC#NXA8KgWJmb#e5QPQ >KPh# 6҉Ouŕ^$Aq074LK,0/J*;͝`; .u{DzU)06UM`bd hq2 NxxWgf:َܳ+0+1VG5d5șvܲϒlC9il%MSj(5.c8 zX)ZG4 %%! 2rŞfZ$y+z؆ˮg{z_N0LP3NMK*&u&z/IT-%¼rXzʝ}]l!j8A+,?(1;QKAEfZ/^Unڿʎ&_pL4P\)r'ܾdwASĎ8?vq7PV=jHQ@QtW%6EI*vdP&*܉u]Nu5TT0 --fv9T8صlZr{+LpAec5C{B6x]n322&!ŵ⦟*rVMJFM+5䂇JaѻU`j@-fƊ+%CH డn>\#FgEw_f<*LNNEc%ƺ˗Y^ K.EQX7ݪmr U U.<JVdr`lEh */z^> .#fe0OE3w+ϚbQFjo/4ܾׯV82"?E)Eol&CǮgQL,x?;)&Z]Sef m$(& .1jr@p`:2'F9^`8`d;`f:^h0.O? t+.lr`؀D ڏW]a`9 CsՐK,ay6څΧ,~f2ENբѡRN5\hV]ˏɓ<|Bfϸ^֛K&:1QּBVR/_w,nA-PX';V֠b@ VЕbbEY }q42気kv*gOt 7m}+/;)67dvHq2KC-6+9Yi 6#6'.x(XG ׿C7XT;vpyNPLGm=d%#UTɩiOPLOgP\8x`@~3,Vh8@K 29Tx5+~;j^XC,lg;`J*tק.e%%}ܥGLfEn~dXP5&}7Xb=%nWK~vV9߽/th+ŷ\=Q(+5؝PRk&wfLJ+Ky)R.OUSԬ#&#%3t}ʉM\V3^trdh6z-G|Yq&@'*E0z1ND@v)%صBDk[N,OŇ.(n&UOYp۟B+)H<`f; -[e%v+<"dU\CEfp'*Eřvu&IT„ Y ՉU_ & DXv%gںMD4(Րzi)Ư;' \i<$hhE/LLQՂ(1 #28I)(rnCIZpdgAT/C63~FOU/:+KOKGCH&v b톘*>[9c `@ -z?,(/v&ӜI'Č/+cX o#izAYM'ճеLק/5VZ*&{uW4%*-)WÊif WY8SEeT4@A@?3@ "<;|6Ί,A@ Z@1Ck(a~g /U)|lTa)b^2TOs %ו):7SůSNtDmEAw?p4?56QoI$rd' cw-<.֓.@L¢us=ԛ ŖNoMxF 'o3asš|̈́"KM &llr[hyN>fB'e62-[OXy;z Oɛ',Ygr&{Plɣb9ℷLn -?9WϽO(qrF'7b֛+gM(XzXL~hƓ=y KOS'Z2yT'?]0mM߄%O s7,=y$V.ɍ@Og’& ; <&oLx@&o?a:|^VďJ>fRb26̏nMȠGy- E> cO< SQ;u㴖8oziS rԑl (!u o yFZxۅru;M1H xt`- >ʤkׇfkUc6t7qYȋւ0;BF+aA69Ovi*\?XPOdK~|>G -#fVi<$HLXNu -כ>Eɜ -_NXʣtd'OReLd<ȶ=%5KGjFW}T 'G @OWp!'C iB7hZpLRp(kčy.ɝ<& ѡ')rDbB nxu5-VGu4w1b@,.n68g8q{QSݦR:[=!}Gdy#e,1[ 3!!r U,J"hk4hNV50b}*|E S:tWq"B 'Ctw ۽zB94~ud(UI_-٠#atjDR8G>-`hv+Lw 'uE=Օ7?/ =۠Ak+>(d>ޢ&`5PI~;ZϭQՉKV@w>UӴxZJEp~#X $]Ι63EBߙE 9ugQFNnD TN!%athHUQ7bwl_ N=I{`Ԓ~hcdOEdpyj9ѡüYfsFcr|0brĤ'R>Jޯ%_o8\ Q(X8?) hCmSԇw3&i>u( nK;eҠ@f>%e5K -B**CѤ_{|Qt{22(1.k86a50]kD:_t$Iԃ5t]3QN:U 1ƭ]tTN$ҼA,eq;@ȿ*:rR;Hs,BHC}6=gV2]DF*XL H$(}bR\|0h, UNCTQ~rD(N?@]Me l͙(E ~q"5Y! 2r qtҤ&jʢ`%fcb&N9pLL5.&A{L;2ͣϖJ4~"nZQ ީI wbFwfY#r-/:hHۨ]9˫)2X?׼QlGSO"C"GV5fA}U/%u{»;Ӓ:et)v|Fjd(xoNC~Kͪ2u}LB$*e[`^ƀoz+r7X孶V ARc$?њ ]=asQmXF1 IO_ww-AmN2if$>.n"EE* ĤB؋ -X*)Π\Q=V; pQ[OOβ߃HSϯ-@ۂPf.)xS^XpّLr:GB}ljqۘd " `wn0<~~. 2u>uérL[tIwjR.WQPҒDUH^V?&Y 2}zs͒eFessV+2WT[$@ɴ ES|J[ulTOךhmRfG <|7NTLXm.g]  '&XbQS*) HeZVE̋A%j^ -=y<ʵhS?qˁ}mBQ5 ∼yPN& Bl^an 97u%#q~r)'| aL4/D 4+-:H.qPpF;~[X}_ymrO.Uw QԤqU-_Aor 9YW(*=cb`*-8Zf͐4dkB O3:+|lscLUOy#}N#*m)݇A -YwVM-B bjRy!aˀstq&ެ Y'K(5@+fhfMJz{8Dx+bPS>˹sYO"$E6p (✘ш{ &EHqA+UOsQTFlA.^ e -F=+hA_vIg؄Nwo`$ 2.ר=Q=[shT"uyqAqa5ci˕p$; -X_l(jpoO"2ddʢ\RUg`#˙)=h&@脥Qrh00a GQ!n/. 3)̃AMz^@q02=ĕR -4aLtӝ$TvdxOAneWMt+n<,U G Hdf3 kwGwYXKE7j ɇ$M.8\aɵ F*DG=pcNVf LlH̸ -8'["Νo 6cb mѮiE72E ,`tj`.9 -̊wOt'Q -,}׭Q`IߐB9cuאdoWluIatcW~Ȍ{ԒDZNSa``-Kt DEɶZzŽ| WM[zJ99ΓBy0hxS0,H*` Z9Y-MFv"f# -.sf1[BŃ:3s ND5h@F3KGjKQu5> -bNu2kJlZyXa-a6 7 VyG!qsTV蕨sA<{7,ivC,pK#1יRiedZ[3Bq,`8xT>27eSx^ 7ʻ(DlWOڨ:uK4jh>zb] mИDN SWՃ2,Dk5Xj.pA? orN,/С.?)z߆^ FmFS S*2g p_NmH˙OͿXų8%HUd -wDv&2?@9XI4fq$#7x~I:CC2w rH2s pmIB(!Ӗlhj $O:V=|Ai8#t'K?ib@I^`D;kQ+ ܤ|E NѸ5a]a|}PzO }DV, s %*$OqLp ǰ<҃$GIgBs 8\-TK[x#8č"kd-T#b;IR(H7&Hx+ERZV\(! HeX4\$jH 5"EZw#n< 9C:w7w-Ђlor@/C?*(C<`ŠvRa(k9t2q]z -31>FzluS2 -U(Q( icd(YJPRp2|QӀrhH¯}.?5ԝԀI$'}hVis>}o*^`&si?7cCXh\ r1yip 6_.`̒cLFŭǂ~ -&j>Pʸ07 [2X`Zs-mYvɄO,xлkaT$pxW.A@vPPK7?,誃_L󚙣Y['ʢ:-KzhPܱh #;d^K.aȠ]BJdd4HGq|Op$ -#qK |Y:| 6Q,"v)M`io -ͰGtdc(,LG{\!ʸjAt( Zc8Xp.e ;Wo@ ڂMҀPr{G*!#ឩ3 -|V:)Ub\|Oܲ'vxTa0XkJ4,Yam{?c$&t/~S^ |& ໽@ӬFSɶ5:RbLyB*FW%ڂaP z)Pe5BW0$'{jX8EqV3[$߲Ӕ LSO{A2>Lm$6hYNbB/ EXF6Eu<T ,fL6qfZs&sW1hv abyP'A__>߽_1qwT4.^j(WDڌP0fEJc^6NTt0bgb7'so,_zf : lƚK&k:pe-Sj&k.[Mw## x>YlD 7M [e9Noi Sml7VrxE', i&ݩ Kc릃v+4;)KP5)D h>*vJ F['ܾ>&~})wFN > -D4@%jnZ]w]zp^P[€u1r=o3vQGO,Q.*/Z{[]_DhL'K|)O|tFzT6nǹ? M;=`2X_&tLL\obtBpIuzx%WFH{C8?9vvx8!qI;kciC=wՓzAnDu|ݣ+E¬.fRi VF)5#EEBig s,i ( -}~Ԋ1Sp[h=~rm`ig(4T2}I 0cV5@ʾG3Us.3p02No^dzuZR'R_Et' - -bz|VOу|D@UuzcE[*S&Y̦ `)Rڔ'cv#00u*M)k'D'Gטg-QՒ\m1$`90+А&Id 8g<2~%)e5[&J=<4  -Gp(g4![E69dATC=t.=&P@ؘ2adfpfaQcZCh P0IXP4@6{aa}QюPCQ岢:pf $d8LЫ -;ɳ%#M -pP--Q[gcV}KpXXTlU+[|Ns+^99+YʘY}>nJa=:G|O:⸹zNJ8C ѐ],fKJϋѨaaa)bRc=jHڐ@{ I[8d$O63ON@ @ƒ -$<(<`P@6@8@ "Dxo\0@ƒ $9( d@OP - @p @x D ,DPHH(A+ 0!",P8HT`A "DHHg A(˪h ʔuMv ѩQaIz[Fgƺr)%(AŃd8@Hp | D(xEѣ!^{BApP$%:8'>ۛLmTw7^Th`C(_\hЀ|{󋕣[燤q,v^'os/6*h@0b @`!'""̰ƪu *i,xo || 㬕8C<: F&7^4jPNr;!9ޔpcNUcmH|2zuXxT LjlzhAӑzEHjE#clYvŧ]ٴ%QlwЖ; yvCm]AϥjQq gTf&"AyGeu5:l Ysa۬WZ=|U/z_6'^QI8]g<|M~jPS)5]^CAo%7}N{o6}wic!MTwPj]9jM|Qra]dM8}YMlHe5Ƨƨo3,bQ߻̗(۩ڣr:Y?svh6;BS,af6f[{ُwqyd |fh \fDn4GCY1d Axԉʮ%rWj`@?n{Efz[ul'W]cw\.c)-Q.HyJډO7(O#ҿwr5 N SYYrh"9u;oK\Ϭ ?,+iMp@^1Y&b3]J|p@q>üI\tD=.c^"j\2w 5!SlJDBz$U󀏎FLA MzrٚU\VKN c6UΥT8k09HE+h pi:4+vZԉ`vnOgC3/oH`GUFe@]qB6qd9x6&4LP(|r>5)Y9N*˥Cd}`E_~6!- m%.l]^IhDIqĺ[qi_s{=Y+_]"f+Eҩם f ?v2W6,ѫƻđ ?FvV}ix|sMB@lmn bK1[T?U8QUat_grXv?~ ̎ ;9O!cŠ -B9!UҐ7EN;tpz%(,9`+uZ ?Q؀9ܶV(g덏#'xgB\uVK8jwcAJ|Iٵ8t !xsX:,a1n -c- ?t fMZ˭@5 Ch|;y -k0+VEVbj-&ϩu}ۤQ+6PtMViݫ4oM14UԃVc\fUIϜ/ƌ5&0cI!f}@[ݜѥGm ɳ[l m8>;M6ȱ !t -*1JXMM}2޶\t˳9OĦdž;c -kP:' ERIsyV5=Re>ұt" -0VA >foVkjsg>Ts֓ʫw~li7#:\-2?PURW/w&c[cIg?> OwgDdX <;?9$ pƪ D0RD Pm_?:hꓙ0: qQBQ;Xh| DDQnc4bDl]Qe͆_sWޠdרa$U yb!c -r9^j(l }]~3==Q, X\lƛ@޵J/;an‚P/~@KbY8')` 87B  ς17h+0;Żj")ZX4 p󌒂mѪF[_gZq cepDQl?p>qdZp]CZǦXjG^uG꫁Ê%#-{~aq>pOQ,MyɸQ9׉}PӞB Qfg#?y\n@TX0L@+> eP_o`yi3veFiLwk~GX^, \+#z ٘rOD$F2,6(2BP)*X[48.q4í?nW$ϒdޘ,"H8`zh3N?kZ\@XzgF轏rRT|0vs-U#45tDGy tЛQf)mTt7K(=<*IOT]2C9:MV(XoI@T6p()" -Hfń4 :xfZ3Mbv@e>{noy<hj̆Dz&KURC*=@`Qç(ʃ"?Kt7"lM3=:shoMFߊD&;0CR̎2Q%K -]wJ cކ3Tvs-B=J(،W^HȓyZ22s,mj ʹ^tvZ`z[:R#];w)lv7?;C>u!ҮcݨNh;\tT:vvtr`C .3gy`#p`*\ !ckj#amYp<-ږlᡀNqtcҿrL\J jZ_Y_+gԊ#6ZVnbff(A[sYoM{X`3n8/ (%DKr@u&V[$_FX`) X2 BsoL0L~O5vVi_:i=h_k4 - :4r^i0W6V(ĭ7+ZX0LW#o -A]/$`BQ{X&/0\b1le~:(E mZ)."|p hɴfOƺ/9eSAp!'5UE]q_xpꗅsA[P)}tTGeYю/%沄g^ʲ_)qъۖOUjv5V 6qWl+dy!/؍q^a[,M.LrA=!ڊz -Q<ݠ+8'GKrUi:J* r>r=~V'3zj.W ,{뵹"DKuTE?%g +ю_ΰF`x%PKcCN -;Wi8Ib2%l|q!c\^LP5$ґP{A%ڒ LZc$0n$dk$ f^< bI3Fs;0"l; w,c"6@E|+ =@ZUh6E (%UrH)cꪞ78nS#H^ -!2޲bm=(30e%zHLj#aHBؐ1HŸN! K,Qg:"a1q`E,JK!MԺg?%X?B<vg-@˴0O~IS)˳[ob<R~mb`ژcpEk'7) 䛔_PBޖ41F>J'S ^-"~#Qe })Am <1NbcQA%mZ=c ;3eNmg:yc#0^TN,R3m(['M*>>Q@H ԼA! YS=:? '(u\PP۞[\Em,n7^n?i&V`.57-*57 X")TjT10洵16}D<643߄61 -9lra< 셶(7UB5iYXQ2˨勖/k"IZQ9R߳1_Ss"wLQ+k+L'Fz4u&PRpg-.f6y? 骠{/OtH)yvmrУ:+?hg%W0;9IǤDBr(*D̈́1)BQHtq*F! -/;rBiP[uſi\!W ̞HEȹ eF?]a, j8'ͷʚGuQХ-#N -/E;ʬfsY𺠳MTVq ]/>oH'qU5"Em܇>ΟzrQ&8YVoԴ/k,7R11)X&Д zŕ-ЗOdL0pzJ[Pd-ce+{!0U\]JmCj)ϭ5<ƱSFwM^N5FI?}/M\!Ky_^5-, c/_yFX\9V])|t*Zݝg0'}^soW!`|& .[uFڭh5&U( ;ƪfSEҪ'P#pzVrc#̰Wؕ/hj-9jV%V,j3o .ͪiʕ'ctxūQ̴'U/ +uFX?Ϻ ^zxjiI\iyvκaiz )g/:^C9Tk/),p:-CN[rF`Z5qѥs4k6>VS2q8vn;7_5[Us9ļ'MՉVAj _INtXH 8foDA5iK214\ fN3R䴇$:XRFf sNr(l2xgddѷ,Jjʱ.L5έ&T-2%?Lu͸ߛ;.M1Dir -m,5XX'dIԌk <d`^綜07Pof䊘 J ]'8@Zsf. -'|*PWy l.48 VPE^ c筩gқ>N,z$ +R%ѝ ,b,hGz`/}^gD›y^Ux#μFC['nWL8[EV@ _fp.o;։|cm;w廥c)CRr)"RezG@طnSb,1yLEl'K%DFeDEb#FuTr&HM}ʛ] >rFVg;Ҳtь\2˫.NԬY%F )Xgf+\cܲy%+ffEU͇\ygwТGh"d[G5Lu6Os2j.'/ăXdb6S"4)W$LDB cPND9|w__96F}[DO%QS8&B2eGX+usʬccP^ S+%u3O -K]8Hͩpk-FtYI^ӿ!o&GaD}TaOnID4%=7j/]tL͸M !FI"VZ치ym؏ǰhP:WBkqM(s{*!!c)`o>H0 =d̤#6f/׵ Hq -9$N/4'TeTx 1]z`XWi!]# J(0 CD'(c4J' a~7Přn|qCƴL'΄PK G#H%7ElA2 0\C')1}ĦuSNF%it^NajABd+8D7y8(SgqA4Oot%%Ko1^+fV;@D::*3ĂD"?ELs8:*Ia/ iM#CL,ӂ lk(zQ86F] ߙ/JamD-@SʹRb!/FCveTU@RI~cS*;mN/ޑI݅BdO'>U"͆d\{H[b*g) 𸧴ȥĽ,w!,]G,E|Clu'oߋvPϘx^[76-GV$i~,O%-,bKr0L,a tE -iU"*%9>o\ -!P$su;C_Rd=5<@qJRsnz<&SSaPrggOOOU㠮lcJӅ;)V%%|t89YS#:o24JfLCWt]Y:QY^1J9К5f,-drk<S\՘"r:!ՐӁ]Ȃ 1](}ጊ#\j/a)]CANXJZ-nJ+sn\*M>%Ș~4\UnOk2T ˼|$͊i;0^9y@,'dgc|et`7c] إU -!6 jR=:?<5z\ͪw\+!RE.AR3S4ǻ$ړD7&!wO8.ᕬ8ٳA n?ٟ?^WA_y"=MR/&%!PbTx(v&sjKԌFCB i΋%UyHnj ?7 E6WP.a|͐FIN&45yQ:sLRtg$p,qdYɃkfpl1y_3 -Y֌0-7]_3(\#1]L*/فfIa3\`n 'o&Ei_7.l5yEUŗ\شWgkဵåC hsQ ɳXЪjRFB|5Z.<9?ms#[( ŵZg>jbJFy'oRTtA#r^ha%\u Il*!4o{8c1ÁtrC`!oMeG*[Uh - Y+#ƒ"nۚb$O_8;|{*RȺA$R:D yMk4hX+_(5q(n';;^;9PDu7, /s~aY+$S .V{zDs(srvf"sRQ9|jDC& LM9դ{R3iga*Cлɣ8AH̱GCyg4PI]5x+ Rv}.>nr_h2A^1Go׸*^q'>}kfuʍby8"UOLZ(#/t+*CxBgFB}iAidl蠄{BI(;/ﭳfÐ zΛF/Rz8Dk6gZ'T+Ζ_^XJS\yeixxcAo2mEɣD<4(NanK KkLN:z;vos<(EjlK.+L .glhSw씜䈯B+,#jC};v,629j ~GNS͟\rʴ2Ƚ\ wc -0N3ԵJ2 `NDW6 4ܻtNcQ@c EG?]M8a# -0܍L2.Xk >R^*.;$MU RIs1Hfͅ r926I'LN>20()PM`4zA#*DuW؊Ldr_>wdF@-[ 4cҖ$ﱍ0iĐ'^ Ai6+gYݹBϙz|;ṃwd;Ci>V>#G"q b} H0"!vPaMJ-: WXP$ANt]XQ -Ib2 1;+&l*Wj;dܬoU' H#>|L?ֆ,2+,t+e<ẔUB;;䂔KZ,s\$|rCP t`=l$/ci% *ZC(hE|550!1INsΣ^E4Sh6-D3/iM|Yҹ̛(6/Z23X)P˱'aX]5z|>FМ!QB 4΂fY9ݥ9@UFop\q3b$VJs$;Rcb $;&%;dМ/%:6=L( }΁+I6:F5Im%E&>gf3!*U":2yup#AAznŴz8S. zRr8w$eU1%ޮ3)s\v!iL^J XiW)T4 /4"U~} EKLF6XAt X&\IMHs0J2vf4A[ bN(I壆hMq$$LIvJ",iD˃*煱+*P="J=^X[cS.$9/Za"HlAnaiyxwzn4{bwFECcQ53#;Y kHu?s "s _9F1h.X$"fmb(L -Ps\j - NP<ZiEΞj>A0Qq>sCnj Qʪ /(\95($%nugfmcpI2З*_P ~%M^'>IA~&) ypSgCȈy5ur'9o˵1E -૏"[pJn - -)$%jnZ;8nC:+wgl0PLK@6 `Rۺo+ѡ - X̾DOf,8vG@@LZ}W:%EӸρw(3(q%Bc)޲'0%LrzJv& GͱCHF_D%~0[ Oq |=}vJ42cġk^ї@B=M#Wg̈$QuZb/!cBN Mtlk P7ItšVE|0nZyf%^ ;-I&b@ -&`H#JOgp~i5RKKnymdκ40"-G2&*N94nռ q(IbfA7T4AoI= % %NMZz,8w) C7h K)mNZ& -iK< XV žz4G.flrfܘ.>6CB@g|`z$ BrSPd_2>:O]CI5#cEŚq Bs8L 9쮡9-#-Kޣ-#dſ6-͖ Q$k;L an}y>+@j8Wl@ -m)[ ^V*%X9E /"VC\@1D5\X{ܑ: 2q' -O.D/wVFzP  < :}af(ɳ!ђ` m:NIzp;(5L*֐IBz2vPs.FKڊ(;JcL h'Xћ-u{F"$-)%CRWwLcHo%^eHխSOj("0`(OQ8( 8/0,U .gcB5[0-v7s:ͼ-/dE?5E95&ʆJsG?>58}up(Qy -ǰx""NiƫiR#f/MYH\-Cn2G8m8<Ap7ڲyF(ވy[= HE:&]bKZ,mY 3t8(u0[vn4X9_4q]C/6/'V &9x`]nP $c>zlƏ]},]II`¦x7v* x4*`3ċyEﹰVRоeWEo i=Nǜ(C(#, tPhy-fs8Aa'Ϊal!qwK2k.:r&Υ:rU0uya.@. %Hrn?P:<ljaF[eىi)̉RGk74[5@\Ê=4KY$Wc8,~YzUR#Lϔ#M%A]Ս#KwUC!\ ]C}7Q~G;A MLLaqOY13N$.FЌj#V:Jjt\@~rWH(uL{LSS:$-Wa$\#:"8oCՆ@o;eU;2rzA,oZX% [)=A 1,S}^Z`0D)G#躵}NƄQ!=W]D-:qjjgMvhrS۹6eZ<[ -Eǔ0ǚWPTp`$d@/ɓ*ǡ|1ѱ n  +vDEa/. o#^dXP-%yl.)e:{(G:Sѝe; .ik>^t9Cl] uJyd9@јΓv0쒉ըd-xޠ -W;!-i<*nwAB1ͪru -{@;+u6Th̜3Q/ {mf8`ߠ3l8CH{/$-9UqCdvgV}ݮU]ۢɍt>McP1wFMdņnP mQ<4U ۽~଀vhll{kM~P m(ȃ<2 /ȱ8(d1UP=of9"xC޲AHS$GaIlf1MK_D&;Y k|{Hb-> l+\`N)0Uv*>OLӃ+JBhGd(ݜr)t}sU>U)wasDC7~3%r"Bŋp1{qR{%N8v? r$(tk;92yL6\Չ -j>IHv9mID(FQ4EFiwPCgЍًS 3CL* ;" &EHUlTAPOZj")CJ9|kSLtBL?@mc5scxo 3ƍ`97N Er88;Gm{o7(X}D^a獴!' }%Ü̈JA>Uu z݅){r-#Q i0lA{PUV˻5/=sby3 /Lv&" -$ I9 ZcJбMϗ:8\*?lS7(n& bm$ArZO.)LU ~bT RW97 ؛q6i<"a, <(3 =|k @.. -N@[ņY*  *3O_cNQ~Zcf֜j|Bu|LF밳Wvɚ~ɟǼߔS$?.iZe@Nw4m&Pl\x!K|+fO/i%-ǻob!mD fǑ)GIu=kUؠˊWҦ ",2՘h,3Ĭ -/H<d 2W x|O[UN*炞;M!_dM&jbP{&IG_]4qBn0%nǖl:!M@prʳҬFY{Z.ֵ舌 5 FϾ]/u"VDOKdH%)ϨEJ@@n}0 \;$Tt(G-fiaK7\zn,oNP.ɑc`L1iBNG쬱@](ƫhB3t5 mhlz-jg_J*s-$ѠîU!kfa"p1LTG1$ +2n)FsȦ }@hc"Xjs 9n6CQ]J rH0ĥ)ɢ8ӷJibO[L3LX7!#H!{"U a -t,[sKhxb1: -z^=hЭW\WsVw֬^yZ,,gsDHW/.5"%! -Y2Keg=uݣqs*N)+/;aqL - bNY0`cuyՏI˾ >ѐs,Kc`)֘?-$XQ]\=\V|\ [-A+KTWLuo\XF. O-9,Eb *2Sz GԮ?]G  2C j+Eei}*zPi/"*әഅcCk@/fV]NsXڎ`@`;{R.FvEtsi< UϚsv Z-c35s>^o(YW:1k|jW:Dx 57{qQaxrplP( !WX<5t|Yͺfjجpkb)v<$}b{]Ezs&Qt^,;coSe~(¨~-,馻r5Yz^p#̳/+IR<ᑿbʁtrj.S8]%SA,PI>rCs ,;PxUqUhx|}yEUM-#O?gIBq=؉CE؀#j"^ n-%$`" - e8 W` 2^Vt6$y6()TBg9!ږ( C/@t͍d{lJSkeG-8P+yo%)jeX1)BSq5H' Oew u<uq~cČGj}]bv#:O0> gxOXk_<Ȋ ˊPP~%rMDm B@},^Kys peh5[.d4DwK/{!"MI KU "L[^ 5߻"679T2S}CeZZyO[(+- "[ PhN̪Y>\_8a 'IK냓dO(&خr "'lc -F# Be75.X d+냬Aa-w@4(Į9K`uѦFQhT'V1egS7H1=#T>ZmIے߽jbp(hVw:=iiXG:_[6"c^:.^3M)f}58$đ('  -vOK$VA_ ;كNr dAxeS uZOËfmOdY7&u)5Nx 3t]'JC39N(rG$u?C 4Þ!4:a-Ca*$-賧$Hh%1t#[X"yIR?`,r!քC%)BtnC18 -Wۊe~κ\S+(@A8C`iaO bƭ:29 ++!>WćڷG|UWMgA>RJ<;)k ?VO ,Ɉr%}P>{ 6( #~)} ْx  D !fs#-/$#~ )IЧ˂h(SB`#Wd> =AlTH݆-4"1w4 +.˯6ˋ|2)=p饸nv^n337Qf]'|,%5hj΁^[R9~P& S[Ivi2iX#jOúFfxphJiNv}__j7<{o$i}>`^bZZSPwp"#Ai2.S p|a݉FzLUq !ftś97iMeb 3*7$&i>kAq2䦃cBM՟a5)1 Mw9 ('?nf!97JѼ%οOkr±-]Xs5oR#>ׯAXU>e6;}l%1Swx( N7g-{c -8OH1X'~޹B-#HL=GK.bFzbvƢ>MpHLkKhAbE&gŸKYV9zn°h!2Q`M4܃%WJ26t/ɣ։jtnOj׬x(N<zJQڍcFMlؘ-bJNS4וJ`qK+JB1b.h1zЎdv~^E x"I$(Zх*-ޞs7WF}{ -Kt FqDb:E_BON4(xnUs3䲛)n֍7vdp@D}'s~z{)mMlOXP&lB qFnsܣqQY1v\M)G*nAHMM -e͍p8;;/11XvRMJd%< ?5OL+X}'?q粈gݷ|[B?tyj5}&N?Mq&8쮴edz&Dy`] BDHmxYF  -, -:\vRcg D@I-4m?X C"CvۜuV-k T 絴1y -ק5%k0ۦ:)vVm~Mum+s)jaX=z̶nk.3*c-kSMkNȖMFZfUӮ~>= XMKïH5uWsUwZҺ k ޴ec¹7XD6kKʹCRVignkX4[cK[=ݗzN$k. ʢeg$W9+:95VE -m>Oehy^e11XKB?q&~*޴m4Kml(8.rq %ɽVΚdUQ)`el;^5h?ݗ0Vek0^gYMmUήZ1: 횮ڊf[%fl)?u9l kWU^cXǫγ4ʆrR]c -C(^U}8<1".Ѭk\[-ͽ3W5k77^5SɳLżP3Zgu~cWlmik۹EG>(g - W<%*V&PUd,\| -ol[<Gri/<l*ͪHp - -vdz9Vtϴ5Z!mNr -w!elpi JmYL۲SX$fЊYh`=:,ekV -UŮ,c- HA?4mƵZx.ͱ*j1XYd\jc̢eU[e5Cn1 ({gUDz߽O  -kM_d\s޵n4溡'Eb@P޿&PsT󎒚C]d׉U1{I/kj,q嵡Rb7E5O  -$tuWˬ~tc`-jCݪ,VmndפbjliqﺑBhK kR7"^gqHkMuO$Ѳ -6Yܝ朗܇(I$Yv? NnzTocG<=Ix?Na?9B:sq@H&4uf7}9v&=z7}ק6S޿6{_4iO~]7ǒ6G/.'*fMS'f6ͳߓ'7Ŏj~?M>򑟺ܛI>9 OYሒ(yړcK{9Ӈ/r͑Ѯs4ы| h?ڏ.qK'͑$HF,IS7P&nr^M^.Qyတg/6It$99rcHq$qD?:~]X~8PrK ҇Xfi~sDM$G-?ڇIMv2'PR2dIԝ /З#Y }I'GOR2UWLZ+D+_%,5*Rg),`H^ e"8_fHLl(V8W` -@F07 PϚ'9SO020X~dQ|DfiP+F$<$@k8ƟKKdav6b$RѺVԜ:hTmJCPE>Pۅ6 ?),,t JH fF0ę\/Lb9d/jYb xChJ`|IfP RpwS@@rˌtpqϡ*3o@HPɁg8wq>d4^doOk]jVD4K|-hE 窠;T v2 gu>4ADːq)Df d -Bݒ6܉t/jȝ6iu`˸@C-e 4I3(`KInn+J"hPVr3KTR4d) H(j,@MY - ($`K^41o]J1nʑ~Y}mѶVvmimV;rmbo@A^xUۻP=~6Rb)%TϦ.ffOt"6ć~mY8*d (q"m Lخ}rl͈AL]F9r߆j}D9!laLܷ +(+⣳o$3܁߀@ mc7lQ؎ۇ½wYS Se?ޭ.^֩@ -ogd4=,{43od -"OY!I! ƝK]z% -g]PBq`bʧ ӌ}d[|b3XFhXo:VXu(;]By:AAZ -6p1%j# ƦQ88pw8FAlg%㹀Lt:46E:e`0ڛp7 - - -*J7a -dU2 {@p 7 ƥrH"OIiT1IޯXAs {| {?6 bo@#9[XrrCA`A`Xm(7 |u,A8 K+rLr|D_H<_,b\-4!8f{ ]v OxM1$Zȸa p_ޛAo@0A{oMtR{tRdـi<_yp62{+t-Ԅ{o!N*{w[k{džwߌ{C߀q{{bؓ9`lU,`}0,:OB I36O{SJH 5,?YW#Bw7{e<7Dih.[i۪W0@-}mavnw5Z*)"J˶ -Ьm_&H9P={c61%Ƙ}?Hݍc56VSԘUXng3PL75,igH? O;2yxuqN,իӭ5V^-F˪vv_V{Vl]]{įfeZegvjծzEUeiUe6d=ӫmkU^mDm%utV[Sb:mde|if4sչcَ k-k]6XDo(ڲ%Vk+q?˪ޕeV?yƦ,5y3mf8wW42vW2.>*v{˦TZU;KYbRMͱ 򏜳8g2ιQ#Y`zmYlui2EIop2=TJkzƴr;v~C %+3Us/,+xUX<1jlMX{+SIYظ5;xޅ{qiW^IZ dfmY6^g]Yn³_3 ѼK0ŸgsOrӫe4ʿ 4 v44`pf7 ≮UREil.001ǺtdI4p%a教XP0F1PbZ- `0Zɀda##zr]D -rk oА`W7`RdZ ~HU%46'c)ǗeaȀ7yM)hZq QĮPN: 00^ ,ǗBM!/Y]][ t2DÄi,߀fx{1s 3pwz( VRtaQ3MR|`&Dzgd@=_OI1&:=0ubS>BIqo@(^Q$1Fy`H$4Kˣ`m8?} -NP!E̍ɼ<5rnZa,%_@RC8@ {!p/f/I36؟(fcl Fr^9%H'RLY߁ rbP3bC7aoL<;OJayJz -C.z@ֈ lIJhfl3 _d YQZ0q7 $ۢ0vɱH)16:Rl\$D#WoC>ob`,``u*Ra)`uO|d/#E?$*'p]#:`.I hH!~\"#bip2+vcB1I)vI po\(Xm% zH`73"s|%58`\߀0ÿt'8jZy ` ##b`@(V͠A#a#dYhYfCI@FFazPcwfgR^}e|jYX*e1Qee8VDT)zS[SZ, b;Y],@eŜe 2e+] 1 $I = gVKDQԟ`Xlg9gbp;˯MaX&0=?I>sf[8mZSyw*f6& q@6^Ĝx+h-mbܺ+,yD흭kq,bLaYmi*2,wb> 's&^Z 3qvi e",*n(]28w{\x}B$)䌹xŨ8WS7)o@la."1ۛ;&euE xW.W"9[n՞@7gR,(ܬ=8Ȧr tD8N#`lZ*+jC)T|kBvT&i>H۵2$猫\6jrn:%7 f)#qLb"6;$ )Db* \0\wSn3*`M%{=y]N'uZ6CB.5-OitjGbk"p`e+s\!XȴNQ\raxlVS˲2 0p%jG n-MDhq΁ԉVa,U@C}"C\$ -0ŧѤ4prt*J8hsJ8D&4,o`EU6J(G4Nɼ}  io@hh佘shv/ю%و -͋.y!]C@*)l5GI+l]lQ|Bſ U|&<3kuPu%uPHG&%:el,86ഹ)Mcblj"\l* .Y4iX2|Rl23$3cg9Üp+|K!0܂P6tQ,ʅ[(T"!ELl2XP-s)x<% ~(J!SqUbw*pME(T-Ef -K~ -M(MߤLPcp,00DCV HpiJ0 G "Ei,H0"l*-"t% !N@:U&`oD,$@IIv o@@L+GFq8p0NmV*`PuQ? ۤÞGd+i53gS.zEdLv? ߀n7-$@ >ɔD_};,vmP&!bq<O]r.~,9n\8%>0Շ^rxȋ T; ۙ8&8ݘ8?%n[rm<;[&v@!PLi ĄRWAp2ptL*婩i`on677mfKB";#2Lh4592ksy.To0cG["WDP)b\A.ywqņ_  s\8:Is0ʔ f_ t0\T1^K1F!{Rt*jbl8 2xRLg1;`XXP$yaX*(J,%{!(8k4*iYL PDqe!>z3:24-u9c4_+~^cTD=ձ2 BP,ACU&EJE8y8y 2 Ls⥮$w3ˠD QAYFAQ@LiLT_cwt*Ǩ k" F~+:7 ȱL̞y߀DuNÌN^uȉ2DZWN23V,"+eW^7 UfRzʢWH;Z>#Ehĸ06\jk|B%,a|HFnY2hD~N4" DT 6 -I}8"0 L'FĐÌWHjD@_z<a -~Hao@p0].\W̶z&Ӌ>ڕB$aP@yxlQs~tqpaƓ~W(G7fO--l'${^,rNQ]G" -4ZTМrCq_4*i*+)ύ(pL uIVCwbiqLWC6L&6&RNpipXn`)bEHLR@L%jGZ@.\C:)ݓxa}(Â9o@8xJ1W"|crG4h}t}0JFK8Ђĉ^ !hI@sМ =hK\#87"B## r`sZ4N2@$HX  -@ - !RH'5`SGB FihPEXW&ɍfL+MRwi0ޒ;3^ٝ(}:"IX r]a YW-G^<: |ՠ_KJ`4y’q8KBA/x涩@zI&c˒+dHi?w4%H%Umy}%Beq@êʋ=VwͯX~8(1Pѩe&+O㿶m-粒|Ӭ{ 3<>1}Y{lJa}uMB8͊и2!XtE?O?y(g-u#}fLna6.uLv~܆9oIXXJy+Yuu䈥} I %XV{GG8 5i)9FGqDnc$>a%! -4250F2~-LD,Bs= -!!v3 !~b["BTek̝>O;"z^OKC 1ÞބbO>-εL 8Y3Wbg[yb cOe;ղXs]J%Gm\}x SA+է.0fxLm1}iFcEA1q 8-{6890iS>G%Hz* H_aML>F`NmAng:OU<:a9҈_C;cpl\CڜEzT_x -`!m̡9c_L9ōڽQeռ@8ɨPPR}5Qs:M"z`Ny E*ÖIB$gHnyu/Nwy"'ˈprkɛ{8N oot1+$I<("7xWvFAd6Ӏ+(wB1.(_~C+M_4s$tgƔ 6-wßpFgmjcm'Kr(iF^[xP0,i_,W)NYVej -H\o<3R@|\-l:,&\0q!?,0!pJ`DpW .`}[gJv$ս8P/7 _nZ>V!:Y25]sC:sk)|ثC[0^L1h3L;*zDWm]ݺIE9yEp`i4kHx 9(C2DP]D=0ݠ&(%TGʊsB$gcْE H>3Akk<' uR+,ߌ`t 5) gR03݈ -(ԗkl.t9?`.?tr(q0spB~q.=-HijJ߂<3&pIHCSS&!\'/kwƸ(j͜~蚙p,)a1-pЮy )jɥ?CB5YCFU*O)r Ĺ!>_aO],9uɚ XdY7GXt2N:ub -t!7Te芄"*b-DO$S1T4T a\Aa -b1t7G%A3cb?S.ǩ_UB!p]Xh6Z?@>  :"Mk /-Roc.ɽ!rQ?2 pDޢ'GCe?T'(tBvc!j| 0bcZ)uOX 6 vagRbP6wlƭykQHyGg$!EO|Nud܌ԚLgi~_ߢQlt($Ԯo+SHQ X!kZ_.8X@k^a3?댥[̠\_TDژ)b-#$_Ld)OPLU58L+?Kpjfy,f:%Hux hR ^*餈%PvpjR̝U*Shm&'0+lU*] ڼg"d .V1{Dm~CE8 L[]20ܾ׭`t("W1Vmߵ8B0:5Zs}eG1Nji R%Ἵw WLa $*UFv3J7x 䅿X8. hk_׻;Yp0Ls[ȓ#ZQU}iDQʭpI]M bݹwBf.ӏڈjJXYF$<nϜρN"84\<8ES -o I j4oc2ZO2N(3+9 r}LSxXS'Yf@e]2zzLY_".%$5"_w# B3iABۆ}Mޑ]ElS<7i8eSkL 6j!JG&cXYՊ,p '1tZ|1jlO[˪>7* 864"C>/-KrU}u~Z>zmƴ~?SG*Eran]4p?ϖ|c23%II>9]%( *y&GegPYF\[,:cxNڎZQΤƦ:. ],l&zhfKsCl'z|4ga2lCPV.} *{ÈtPxM9Nh#h5h$OXʯz'h"zdk#`, -y4a'Z{7%!HoԱ!XW;aU":p\QoqgnP -_rգ mUhu[AF+4c4;i`lB÷pJBj8~ҕM*^ٯm- yB0:׽oaH^}u P6x'(g Up!c6#|Ѫ:reAUjZ*f̉6)*dÞlº[d5 q2H_|u3307}/TTA|n$F8F)Hg G~K>\95Hhj[\u9`87Z^׷PvW`l'L8=#+4@=W jHr7,= -gNW_H/~9C؛M[ؤm\@EwJG8C ,8pN pW2bjIBQi]CLO|c8u/,G ,z|:!|%MBHO>0U%YN! ->{uaFBa,܂m -#>Bئ'!NZ8n@;M F HEiie5EHhpX{z5NլeV82x5#lXnD96a]s 2S59lw8H'dnG#iG[ C?fCSz<9(Q0N!t"{'II h~_[ _) ff4:8 Q3J>&RYp wŦ3H`s_ii69/e:쓲]J4B|c}9R9* </eD /u:QWۿ%w49 f5W0l@meM0!{?@s[o0 G.Ա'޼AjU. 1Wq\TIysXFyqiTB8@ Ĭ+ȐZjǭ\;r2)C2?smľH˟{1檹Ev RfќʫvmWx)yK6esxD'y-Ϙ֮F D`|Րw ٪]תl&4ה7qsZU&k$r\K'Z\*3>6ˡEw.depvռJ9LqB*ȇ!_,>_8D߻6ھ UњSyՙk._8?,ѽ ! $u<ɦQJftjK&M E FD{C l`'(%XT:oP<'l$C& v${fChj ,eE\?k'-[Cyy( PG/wךNyJ"DMEҌ(\Ua Ch73\Ҍ}fdO3L_}xh"|lf}l&TU~}V6qTk<ܘ[t7|oK vH컿d;$_CH6:^Y) 2.)Wo~G0>d= S*Xff{VhTx4sU] ,ny T򥍅Mt1r$p5lcm^ay@6A`?nܿnSc ).e=#ĭI'~;ii9>YADl0.eHS<Ѯ`\N:J}(U Pu8> ѝQzF3{媺f<0UZc ?_3 p0-b4)NLg=i@A/Yܦ>Ĉ_%t9 ($0Ky#2 svOZWQ/5uӄFM}x @3k*$qٯ򦅧_0'h|haK(є~TOҊ8[Dh*;H(`lk><&He -yQ'8_su*U#m׶ƣz:0) PhC0zZQ`VwiLug;rE#u7Ҍ]$|tJݹoIݹQ̤ \Ȼ[1@O -@+y>]E$8^\.PdA%_5_4u%G:nu`NeE80٥W0OWʻ6\[WFAdtu8Ϛ,N2g)"9=z ߍT1h o/s27C=r 2~*ţMd7c8LH ষP^βt;t~杓l^>VT{37DyZrz^y %+ --#)[7p0) cI M~Lݎ+pRK U^ j  R|1iu8k$6TKd%1C%7Kʍ&pZbHX 3,P.C?vrP’%؏RsiKrY+f2&MP eWAu?z_`"S.,,*D#!zG;M$ F=ۡtG`m3>|N5~J mžn IQ"9@3yJi]15뺐o6Uj>:AVc^+KsuDt*2 /g D%fvx;j&a0&KQb#P[I?]A(;vTޢ8< F3,iI4|cjeb&@xu ϶٩ Z6)7]~A,KK jV " DnE?pZ'嫼 -%4jNEPm3c~D'ھ]HF,Ie,|ƕ'=YG3kY=?1J & a, -@RJv2J_i׃xRt [D cJ.PDQ*fJll^lMZ#A~T$\,kwqC3jw12LP~m0'6jKfel՛Ÿ ySX{\<.gc}nj@ .\~qzFDXv@w![$up'&b- 2.gڀJ7.ChG~6Ǽ - >8S) FC?P X4͝]b7jZW /y#J,A8 -;:5lé:SA|r3<3IbZwݢ`.ԌCovLNT(O>=趻kE?8rO'O 55x `#*-롴,I-S¢P14ya&e6[Pn1KWTpΤ΅B~I7h{rh51q}:xFΰ -mz_t(q[nޣtw96-d+Cݞw Zi#7UވE~q -VXdSRPvO| 3aAҬ zƊOD#K*_ U(l6 W -endstream endobj 17 0 obj <>stream --PJ}'fOP}f6Kj%LJЄ";hE-㻇5ZoDv]t3TcR¨6f_$藰(>nIԫH-zoLKZJдTNGf ;<+ >P=f,N 42;$ ;3`ne~Wxmp$b> %|ˆu"`1jd<*w9PziԙZ!ڕ;YD%s-LqV˦zF[0s*ZadM4("""A+c ?蚼B&h6rN -G#x!H!o;``ZځYshq ?6Ekb#,Lfv0|{pu43 ẫcčZse4Wȸh J^lkBqLXDֹ7ke"_g\}HwqP&iiH&{0`sCLv!R-3A:b65'c -8Ss9`^$ƥ.%t.20 <'6\ -'W<ۈ?2v*\%QMnsgx3EcOE""f$1 6ɍ"V/<Y"ji Ԓq x7 d6P;->#,! -˰g a7.]Vvq -ώӥbIsUlnci48ZY8@p륭S3[lo2WVf.qw&mO&WtZ 4_J:vnn D]>re[6F)+jnp2B2٠T K;/.+bB8T5PQJK0,\*uA4:}4*V#6KCÂq {.}/* qw:30: C2%gV! -3a2>iWdSP/DFV?wKDwd fL]JQ7["Ng8Jn\~*L5( <pX!s߉0N#hFx^W~V֜>^ձ]70;kaqM `tˬa -Lh3 -W2,zE0e {CW8!awAi޳$OT2fcxm6꺌k$AChqEC[V(P7kEsmHDa -xPɿc53aԊ1=>7hAGZu͹QR96epyF ٓMFԸ#H RPSi6b*@PQAFkGY( ]W>?&/OY@αU#=%?T[*S^]٧dC9˕6qh|Mhvèq+bt8UxFXo/.mw3?@" Fi6Uqr(a!Q8E+r =m5^&IH'8 B 8<^7rtbk(*'<3@$)h.B$}00\FhP\/, "TZERv4,r?)3%4HMN 㘾Io2UYY7Ե:"hHX(^'] Z7BտL#(X([3CWO )ωu,7ӧZWk%]/9Ơ2p`.<- AϢւF-0L^Uǡ!D:Z%dF'd;`c#Rtk0 - -EJ]*YfB  Ҷ0@)oA{މقXO" @(:qɚC3$Gэ*\>m?{֩?4r5@ -gH=q驓qjeRq'҂o?yNjů½V>H8tk})8jy -N̍ ]1C% 1Dlt-ݖ&hT -n,=-xj'&<4mKK"d6mĖ j̛~9^b7ejd"$ azO?2Ɉ=r^Q) V`<9|m B/\r*dXHAa=p;]}Y"eK ?m@sYKr%rh<9ͶD˥Mƌ !Ɍ NFtFL;3]p"=!qz(t]3[XG O U .يԥK"$@Ì4.SEytG و+a1AldOﱱw^(c2 r  -#w$(Y5H^x|݈~_9c>-5.:!uj: ăKʉ<αildy+MeiyD@öf:d:rjL$* :Qea{F, ?ϭ$oo9,\b0b"Ggrr -'ހ Kd̈́3.C҄29%K15 ̓Yi°Wn j3GD؛Kq[ԨV,}{৚{- Yim:%KPEmUnǀW0p%@yd1#77RBJDHV#_wKEF2']Jn~҈ I L(<ɈH1 - glwH`$Uy&EnloqxhCp/h#"h'rʮlz{uZϊzoZ,~ OA;7O̶s>K>ЂXU%D2W`2x.fb'|p#KYjF+2Yy’4>$; s[?Qٹd$ 8UP94G%Xum"?$2"& "B>a1NaTI%afx֙tcTp7miu2R=4ģiS/UErӗQN.<\mf˙07c"mMpvA5C цR&3P?*vB%,k$ p>fn3= *#Ep@T*EL߇t*+KNjpBS1 !?w thK~ I39@ \jז< be>Xvw@ge[|'"UhDм{mN 2! \#̙f2EᏸfEMtAFkF7Rvr"iVT|Tgi_vk {3/V] 1MAkE4V PO =9zLA@'UG;ڍL6&~Z&ItH?l?,ٺa74yX@(y7^?v#J k0=4yM^41h1bx0RIU c]d(IӨK-8:R{au," z~m;E -8FnG!9qFjH\!4Іdn3Nǡ`Tp.^;\UcH#)@2 4a63* 8@CK22u@1p IBٶqF+\4TT< |m,l" ᾡ .z~"Ü - R@ ᔎ0rL u\qe<\"3vFL@eXXcSi/WG2e#cl$SWAАW7m )@M^,q3@s2-FTǂh,qYbd!-/”e&1>)j.ӸLh\.>6B.<^Tfc#p`X"ViT\:ņNi#G OcC<Rs0\Hf"Uy\GD8I {*@ \Tp?u~ xHb4pyةA !L8N17FdzDB#"2Mf`3NX* P\(^.<dLym3"`bj@pzsr@*!Q3Ђr104`ՓtџxG'LGׂepW@=Ugvt[Q.jfy7=inRi`h @A(U!lryxĒyo2-#Uɒ" -PdI!;rʂ gp 2Ft2@5\\$t4%e&4 a:8WD Sl4 -LLihDghqC&MaCtgB06y\HĆFUP!@ДFy1YJ݌6!Q:c`0cDbŪ9| dWB& AFб|' ehHD(d,/̄|#%RlBt\PJF*tx(<DG&J83Vd.xXQo2}n;ʹIMRJ޶Qd3#Q1Y1cWJ,4o%FVeNQC`Ls6+;D$֙ {FrK 4"L*m(jؽƪ\201԰U\u )?]- -5*!Pp= -Ţ+f i#s ՞#HTNAŧMmP*N2S\.crџvU)jPRc92e@oϟ~}NO_o{ߗvCs9_%AjV4[LmŶ}zw׊eJ}.4@-b}`D0"ikPuHU\. :.T>|ztk\~ygiNZ{9JǸs+qqL|'\?ߊsƶz7ksʌ~ln^9si.u]m>o[i1~z|1mM>-4ԱV){V^ٛ>Lk;9J)4B34լ+4f^4i{--:Ch6ϟv^i 64'B8TZ_[Bgc9xҷ~zqYSOj^BfgWK4U3 S>4{vukdz/گL=};KRJT(zҐ 2Dg=|dH(qJD]X4 ! R@tqևd9]D'F;GӠD'CMZ A -|ZQ)M$8wO@hQ|3K -& >=LpU!%@@RU>Fхp34h\ -FG(l `VFc34ՀT *2RfYL0ӷhA hNc)a&or 6Qh8!?Rsџv^[6`+H(G$XQ2(Qpe -# -@G zu`drbXGadkHV. -FKԑ`wN܉;q'ĝwN܉;q'k;`HwN܉;q'ĝwN܉;q'āN܉;q'ĝiwĹ>HӳΞgIN,R FLeg\p`2\.F-Z iblZ MGiri40k,5z>|'4c*[siӯSœ~fnj/RƸvӊom4P.1{i9ߗ;Aff_cۯΌV;zs3Nwڞ!M1sS>[zY1nl8cjicl\=ۋY|z1]]? ]sm[)oxgKy1~y~?-Jjzڛ -qsKqvAZbv9qN=㶴:;[KnfYqKyJOٷޖvb)_mwb>[;ۖnϜljSin뤔Zu^忿g\o>_>?ݛNwS?9_y/ԳԽ[JK'O/{Ͷ}\JoS*i_{lRk%\64nq|Ү>=S)N-^?m-OK)vkoSvn{OLҧb;JLsZҮq|ܴRZO%[[{im=eWvZmwv:;J/uLϳ^.-⚱|-q˶M'LgS[RO+ŐL_Jim<3~Mo߮_}j.u=;ۮ'$[LS9 :ӻbzksd[Jwf[=qnoo1rۚk֜%ŏ;O}Nk[s.mi>3d6-i9W{8C&vk[:ˊsv+>_[+/- oͲ;6ι~؟i9Cc~o *+}uNi)ΐYmg[kdjmŕfYi~:۱2gȝykiޯ7ڧ|Bbfhd#00rh BZ&D2. Eqh - PCAQ e!uFFVkS -ZC27QETti/ۚIѨ/܅Go_@ڈH`xB!f -v8I146oܧ<.F4Υ0ЗrXy-Ƹ+H.zoyKX-?ol!??%]͉턑R3˵eZ_V.:!La߷A-nƴ|!-o:Ne“.3}"["].EAK@|LX#vg͌#?)\E.]" \cEnTW.VG1]xC R. \_p.FxFs>X.׷\L2 ڕsn}nI+6lPzܝ03A0ю}н0,+uʯrr?iJ PO {,p=XkT'9`76+ E$e>BU6]S:ºQ26eVne1v]@ثB_' xK[~x?rn INDca`Fk vFfd4 JdEtpX{IV-! J.>[)`E.!AIKW FB~ QK5̳!mz\&LyPŜ`:H@7wWRBɇ!“QR)N}=-^qݻHsbyg .6OH)"P>Daκ ڀ^b_N \zNQvxJXrP7Db$ $cs1d.VpнE0U(Ϗ72IqӻCQW.q?G-`7n!\A⠅1}||҅>閿8r~}sܲaP ڝ10>] ^֨hun';:)dkf!H.b~HMvofs7~P4rzIzּFJQE#X(t=MAG,K:5 UڠG=]9eT̸wNvL ۝kFJ?b+؇Tɒ[,ăKGOI".2Om06`~&@$ aAH$5aUQ} #$;zaEqdWZk>wp@Hw|=Tp%]lr9*m/U3, Q>KSbqwՕP,Yi$"JaaipZL5gfC00ЋZw%]§/i-yA1Bde_)SGU#ڗbt[H[Ce\P!&pNG2xC_c/ݟaG](8MBݡܛc^UzV&T 71W5*S&6 p򓉍c~ށleP*eӾkiWa%_A2[g@^V)GpV:aQB_zNF1kZM -q>{V@< OZLٸKJ /*d[6]:p.r@ תDu$_ԷpnuMzd`Fl7Md+kA"ͯv":طHC`*?Eۣq(zj4mf^^ȝc&rtAmOp!}\ޗRf«&ΰU0_1"a7K;_zt~?4 y_|lPҷ6A)L ͎[?ܬLp:njY26\ij= K*"KB^N;fPe`Sk{D~`iǂc;2 MJM&NI!8̅]fm Y37ƭ+TpY$].T#?^̰Ǟ,"7OjF[m&|W)HoҒ)M_$O0ԅoB\}wMU=Ls*(?}_ck(7ӟ׵HQ\ OvsAvQqs+.T. i'>7kkNQRΑԯB> tbw)a,}IWhFw[$2aZfWx! >ô_R=Tbeq?Tm!N|VŴr~sfxg68~^#B$C!OS[ۧ SGv3O>eAwF078~<5}r\{"1a4Ƞ7=L\ -<3*,!zZIP J,rL#ͻ:4ތu#Xݩv{/G'78K- d/N&bs9ҡffp^:5؈@Wo]3!@7Lf5ݘ_E|u$É-9G_'=yblkцݟn%ɗk>p߬gqIӎ3Btxyq, D~[Qbĺwl)_`Kc_5N 2ق6#/O}ȑN"vM,rZ!ވ zKtG.""5twѢG`` U-zV2CxƁ!K6(\D 6_NmFlVWo]Cy3Tkby47cʌ z.l;kYE@5jx T)" Sc3^yZBl;0v a<Χ`!l°<{4{6^qsLgUV"Ev?Ĕ9_ .t`ĎyOL+Oi(^ǵX{o$1<:մ~G}V R0nuvdG]4Xاyz@/3y?O -c\ަ}Q? Loܥ~I 0o,j$r%z*Njvx,csOy)e`E1GE#y-'Ld ũMn9)Q3}o JoܚYz=TH,tYX -ޒ h 䝍EF4Ub-#؎¿ip%!<5G<'|F8<8ݛT}8 .ֺv\Ajwu?Rq kvXM~DK0R#цp,$Fڧ@aԦ#Cgwz,*d\Op9 -좎LDӶu4PE"o874UÇ]cf@!д8pSBqVӷXГj>)b5U9BU¤RԀ*IY\QTlN?8z*qN"mMu@Vx{"pLAWHm__H@$g8g9#Xa$h-̺ $wn9CA!) !0H}͙"ǿCxbBΟY\*ݥ@pNFWH$RTV2<1'(a RyEgS/c`A}!pe!iެɔy f]kCQ̶x*+dOPWO*"uUZhƵGP]$b7 ,ߵT)*Mpy*h2x*|dt:J(v),N՜Dե ۰HX}O{5⴫/Q>Q>y>kd;/[{Qv"ao} $ml}R,g#҇gy3 }U67,>Q~ Fu0s<#F7)K1F`q2Naq0acň'&6RD4ԡ"oӆ(3ඈ00ϐG2g~~|"_栱|NJE.QmS}\Y8a\7@:. öDqޣJ[u&s39Emу{dz9ϡ[뒼lOm{.Bˑtf -w^3`׾vbfPOu(Wq7ZdujyƇnآj zg:xrP9@ey  F|i:QK4ٔobfQz{v`Çp,W*M]Hi}j!bڒZR1 g3kqC`GU`KOo WH򊫑^RE OSikU|[.9',KęXL`,QO aW!tpm.tn{~ IVz;?>;wUDչ|vR(NKdvjff8d_= V44Rc_jMA+kQFOQ\] 9:ٹf̄_{D=UPH+֪ϼu9𜪮G6\.R$.TJU@%ϦB "e(>fpv27(^ܷN*˿j:3aҖ2դgtp/,摡ΰe5!mOnMH֬? cU0(L)hL}u5Tĉ6]dx{BBJԐdWL jQ۔;z+ p4" Q׺ްA-xm3"Rh@TB |2'w ^%G-Fc5]s:>intnXDXh|} 'YR" ے)H vjm(2B`|@HU(ɈOҔĥ `Uоz\A]izM%A$$'A۳ 7Bd##]X r* h&eNȁ -WFJ/vk -3d'2]Q -*VI%]hXb7$݃ Jmֱ6j8T!fCY2fRCV.I qmF^MTYѬs{RfB -%퓉$;$Ge7 ,>mX|K:TZp ǑtQ-L⿶-z\Z w%Sqj}biaYҾnf~4%Ãjuk.u]ȷ4}.)I x:y-eωyh_{D;$>LzAC8-M~X_+`Yx4dbrϾ!5 čG c2ӭ9DM[|u)kX|L?D_T"Vuڞ9"<8xͷ%.V(k%EZr^9XY5,lծ5"- *A cb+?;|飗 ]'HR Q7Wf"Zh LOnwhժ4%K"vyIn6M( - Fcs֏]MQt]c 9(ū>zBqlȠQ la2|23LF?J)7+ؽB5esY m8Dt'^^*͕R3WԨOMS1 &>DEzI8\Lt ‹`Xs+ķ\ $ k\z=) | -.jrPХ$]2'6D TII5+7$ul6Xi>{lI,O3wDC"P ƴRW#]2L7#j RݍN{ A N")A"[FflSf -s&PG( *% \TmuT?DoX]3B?.Cg%_+C 4Lyt7P9a -4E0LsVߝp*d5 qbꁣJ - Du_@+59AGy8&vGMToS?eOchIz|rpztD%;~M*< $KEFYZ;w}(LwgOdbQM9^JΖ4s)qȃ¶=ʽ&Ʊ" Yƥ?y`1US~(s)6% Qq1Iz5'>hG$gzY4>D@|wގTV?]]a揖~8$>񻧟C8,8dB̾.p`A"Q.R0{; 3U h܍`s<@!Ӝİ:Y' ~axLbLKEGgm y+3uHr/me聀F~|*5Ff~v7]]gg4؃%Xvןmx:2W~L [6?mE㇢ߜ] #=)^*&"eILrڐp4TȀ]!v8LEr=ob˕yu2;ūt#D i1P-BI Bb7i[C1RV#]S~dwj$z'͜ ݡocL'YK6iI`XT -#Zq=v/r7 -& *q5@4+"7x+EVeo73bΉ;2v Y{m7 H -Y,@'H<<4;`Qݫ$Qi Ico,Y@9UkU\ߢABɝ8+*H%D`/@ojcۻnji_<6@V_.ZE‰;W÷h[x}hο f -hGod72xo8o{ |;J`PzjB+ -{e2>Ggf_M0} h`ZSSse^w\zC:V\t ) _rcIƲR E#]!j"K%EČ*ݼ__ar":dBR$iH _I&YID!%LB#ݙbdLBh«'InI;a'H9 kN8vN7- [{% ;LCv;Z}VMj"vAM[j *o؅*"<#CZBgUQ*Q0-yQcȯ&ĞKvsc*8yKV!gK&Jc2l -++k'+a'(X[h. FM;ʛr+C^CS(t;J =XzC Y޸qJaa:3"JƗWrO0åV;o;iLe|tT\`ʂJ,k/4]W>>V -V*sarV5$a4Sj(-Z<\WlPe $3rPWmHE(6T*nw&HwWB(DoP7 -kZ#MRɘe("*f@*)ģlUvl+V6";zz4J$ -uc${c6تTI2I'ׅc\B^qSY^FȀ~F$m4  _xbNxe[R(MuCe[6u{@t{p:!LuM7Zg|5b͓Hx䌶x675E""qHYA6Y^_(P^*@T\p۞Pq*{_{~W(ml`ʜZxB)|hP*[VyIxKŬiA%&)ޓ7@m0 @c5luiT*wC ZT&WFn wєzNMd/թ2awήU5ν8r\jCbO4h"}>{ejD!=Lm{ y׭<{>J -EF@WhIpZ[rPp=ԧNnL(R %,ġr`->$* -endstream endobj 36 0 obj [/Indexed/DeviceRGB 255 37 0 R] endobj 37 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX$6Ra!<<'!!!*'!!rrmPX()~> -endstream endobj 33 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 CS 0.944 0.954 0.961 SCN -8 w 10 M 0 j 0 J []0 d -/GS0 gs -q 1 0 0 1 442.843 238.3426 cm -0 0 m --15.683 -27.165 l --47.178 -27.091 l --62.99 0.148 l --47.306 27.312 l --15.811 27.238 l -0 0 l -h -S -Q -q 1 0 0 1 204.6208 143.1162 cm -0 0 m -15.811 -27.238 l -47.306 -27.312 l -62.99 -0.148 l -47.178 27.091 l -15.683 27.165 l -0 0 l -h -S -Q -q 1 0 0 1 431.5853 11.4572 cm -0 0 m -15.683 27.165 l --0.128 54.403 l --31.623 54.477 l --47.306 27.312 l --31.495 0.074 l -0 0 l -h -S -Q -q 1 0 0 1 294.4866 173.1122 cm -0 0 m --18.457 -31.969 -6.844 -73.227 25.938 -92.154 c -58.72 -111.081 100.257 -100.508 118.714 -68.54 c -134.884 -40.533 127.975 -5.396 103.972 15.609 c -100.577 18.58 96.839 21.269 92.777 23.614 c -59.995 42.541 18.457 31.969 0 0 c -h -S -Q -q 1 0 0 1 325.0405 155.4719 cm -0 0 m --8.956 -15.513 -3.321 -35.534 12.586 -44.718 c -28.494 -53.902 48.65 -48.772 57.607 -33.259 c -65.453 -19.669 62.1 -2.618 50.453 7.574 c -48.805 9.016 46.992 10.321 45.02 11.459 c -29.113 20.643 8.956 15.513 0 0 c -h -S -Q -q 1 0 0 1 359.4413 148.5375 cm -0 0 m -10.619 18.393 l -S -Q -q 1 0 0 1 371.1792 110.679 cm -0 0 m --11.352 18.443 l -S -Q -q 1 0 0 1 342.4347 139.1691 cm -0 0 m --21.649 0.62 l -S -Q -q 1 0 0 1 220.4379 115.8778 cm -0 0 m -7.407 -37.351 30.63 -71.733 66.706 -92.562 c -102.577 -113.272 143.701 -116.285 179.606 -104.266 c -S -Q -q 1 0 0 1 393.8267 266.3006 cm -0 0 m --59.824 19.518 -126.482 -3.692 -158.449 -59.062 c --165.272 -70.879 -170.031 -83.333 -172.849 -96.021 c -S -Q -q 1 0 0 1 447.0169 39.0557 cm -0 0 m -9.78 8.879 18.356 19.374 25.294 31.39 c -57.566 87.287 43.776 157.415 -4.13 199.337 c -S -Q -q 1 0 0 1 430.1418 189.9215 cm -0 0 m -25.677 17.19 l -S -Q -q 1 0 0 1 436.5098 178.6974 cm -0 0 m -27.842 13.447 l -S -Q -q 1 0 0 1 441.2536 166.7061 cm -0 0 m -29.466 9.443 l -S -Q -q 1 0 0 1 444.2809 154.1808 cm -0 0 m -30.516 5.255 l -S -Q -q 1 0 0 1 445.5329 141.3655 cm -0 0 m -30.973 0.965 l -S -Q -q 1 0 0 1 444.985 128.5094 cm -0 0 m -30.826 -3.344 l -S -Q -q 1 0 0 1 442.648 115.863 cm -0 0 m -30.08 -7.588 l -S -Q -q 1 0 0 1 438.5674 103.6722 cm -0 0 m -28.748 -11.684 l -S -Q -q 1 0 0 1 310.6307 118.0083 cm -0 0 m --14.554 -7.029 l -S -Q -q 1 0 0 1 305.914 137.5234 cm -0 0 m --16.191 -0.504 l -S -Q -q 1 0 0 1 309.5551 157.2273 cm -0 0 m --15.028 6.108 l -S -Q -q 1 0 0 1 321.4745 174.222 cm -0 0 m --11.039 11.886 l -S -Q -q 1 0 0 1 307.1458 127.8552 cm -0 0 m --15.731 -3.713 l -S -Q -q 1 0 0 1 306.7646 147.9286 cm -0 0 m --15.916 3.012 l -S -Q -q 1 0 0 1 314.5842 166.3773 cm -0 0 m --13.35 9.216 l -S -Q -q 1 0 0 1 329.8993 180.3893 cm -0 0 m --8.204 13.994 l -S -Q -q 1 0 0 1 374.852 95.6871 cm -0 0 m -7.07 -14.523 l -S -Q -q 1 0 0 1 362.037 91.5891 cm -0 0 m -2.788 -15.933 l -S -Q -q 1 0 0 1 348.6091 91.181 cm -0 0 m --1.71 -16.108 l -S -Q -q 1 0 0 1 335.6083 94.4943 cm -0 0 m --6.075 -15.036 l -S -Q -q 1 0 0 1 392.4974 167.2558 cm -0 0 m -13.016 9.582 l -S -Q -q 1 0 0 1 398.8436 155.4035 cm -0 0 m -15.177 5.629 l -S -Q -q 1 0 0 1 401.6691 142.2796 cm -0 0 m -16.161 1.24 l -S -Q -q 1 0 0 1 400.755 128.9009 cm -0 0 m -15.894 -3.245 l -S -Q -q 1 0 0 1 372.1512 201.8157 cm -0 0 m --4.128 -13.097 l --0.415 -14.227 3.224 -15.79 6.723 -17.809 c -32.064 -32.44 40.444 -65.366 25.441 -91.352 c -24.105 -93.666 22.627 -95.844 21.027 -97.888 c -32.486 -107.014 l -S -Q -q 1 0 0 1 275.2839 193.4857 cm -0 0 m --23.488 16.337 l -S -Q -q 1 0 0 1 368.392 246.075 cm -0 0 m --43.231 6.616 -87.586 -12.632 -110.29 -51.956 c --112.506 -55.795 -114.445 -59.719 -116.13 -63.702 c -S -Q -q 1 0 0 1 333.0875 258.7783 cm -0 0 m -4.777 -27.605 l -S -Q -q 1 0 0 1 287.4663 64.8066 cm -0 0 m -5.07 -4.447 10.635 -8.486 16.709 -11.992 c -S -Q -q 1 0 0 1 304.1755 52.8143 cm -0 0 m -6.074 -3.507 12.354 -6.308 18.74 -8.475 c -S -Q -q 1 0 0 1 317.9897 29.2883 cm -0 0 m -4.926 15.051 l -S -Q -q 1 0 0 1 361.752 21.8364 cm -0 0 m --1.159 17.144 l --13.691 16.574 -26.468 18.307 -38.836 22.503 c -S -Q -q 1 0 0 1 276.8943 53.0148 cm -0 0 m -10.572 11.792 l -S -Q -q 1 0 0 1 294.3333 35.7671 cm -0 0 m -9.842 17.047 l -S -Q -q 1 0 0 1 248.5597 87.188 cm -0 0 m -15.426 7.568 l -21.199 -3.57 29.088 -13.768 38.907 -22.381 c -S -Q - -endstream endobj 38 0 obj <> endobj 31 0 obj <> endobj 30 0 obj [/ICCBased 39 0 R] endobj 39 0 obj <>stream -HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  - 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 -V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= -x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- -ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 -N')].uJr - wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 -n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! -zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km -endstream endobj 26 0 obj <> endobj 40 0 obj [/View/Design] endobj 41 0 obj <>>> endobj 32 0 obj <> endobj 42 0 obj <> endobj 43 0 obj [0.0] endobj 44 0 obj <>/XObject<>>>/Subtype/Form>>stream -0 g -/GS0 gs -199.999 285.819 304.583 -286.226 re -f -q -0 Tc 0 Tw 0 Ts 100 Tz 0 Tr 8 w 10 M 0 j 0 J []0 d -0 TL/Fm0 Do -Q - -endstream endobj 45 0 obj <>stream -H63Ԋ $Z7 -endstream endobj 46 0 obj <> endobj 47 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 1 1 1 scn -/CS0 CS 1 1 1 SCN -40 w 10 M 0 j 0 J []0 d -/GS0 gs -26.533 -58.744 214.446 374.503 re -B -q 1 0 0 1 166.3662 14.3408 cm -0 0 m --4.582 0 -9.164 1.738 -12.674 5.219 c --19.733 12.219 -19.781 23.615 -12.781 30.674 c -19.746 63.476 l --13.056 96.003 l --20.114 103.003 -20.162 114.399 -13.162 121.459 c --6.163 128.518 5.233 128.564 12.293 121.565 c -45.095 89.039 l -77.622 121.84 l -84.621 128.899 96.019 128.947 103.077 121.947 c -110.137 114.947 110.185 103.551 103.185 96.492 c -70.657 63.69 l -103.459 31.163 l -110.518 24.163 110.565 12.767 103.565 5.707 c -96.566 -1.352 85.17 -1.398 78.11 5.601 c -45.309 38.127 l -12.781 5.326 l -9.263 1.777 4.632 0 0 0 c -f -Q -0 G -8 w -q 1 0 0 1 166.3662 14.3408 cm -0 0 m --4.582 0 -9.164 1.738 -12.674 5.219 c --19.733 12.219 -19.781 23.615 -12.781 30.674 c -19.746 63.476 l --13.056 96.003 l --20.114 103.003 -20.162 114.399 -13.162 121.459 c --6.163 128.518 5.233 128.564 12.293 121.565 c -45.095 89.039 l -77.622 121.84 l -84.621 128.899 96.019 128.947 103.077 121.947 c -110.137 114.947 110.185 103.551 103.185 96.492 c -70.657 63.69 l -103.459 31.163 l -110.518 24.163 110.565 12.767 103.565 5.707 c -96.566 -1.352 85.17 -1.398 78.11 5.601 c -45.309 38.127 l -12.781 5.326 l -9.263 1.777 4.632 0 0 0 c -h -S -Q -/CS0 CS 1 1 1 SCN -60 w -q 1 0 0 1 166.3662 14.3408 cm -0 0 m --4.582 0 -9.164 1.738 -12.674 5.219 c --19.733 12.219 -19.781 23.615 -12.781 30.674 c -19.746 63.476 l --13.056 96.003 l --20.114 103.003 -20.162 114.399 -13.162 121.459 c --6.163 128.518 5.233 128.564 12.293 121.565 c -45.095 89.039 l -77.622 121.84 l -84.621 128.899 96.019 128.947 103.077 121.947 c -110.137 114.947 110.185 103.551 103.185 96.492 c -70.657 63.69 l -103.459 31.163 l -110.518 24.163 110.565 12.767 103.565 5.707 c -96.566 -1.352 85.17 -1.398 78.11 5.601 c -45.309 38.127 l -12.781 5.326 l -9.263 1.777 4.632 0 0 0 c -h -S -Q -q 1 0 0 1 257.9834 192.3545 cm -0 0 m -13.972 9.191 21.286 23.716 21.286 37.704 c -21.286 50.305 15.486 62.267 5.375 70.523 c --4.686 78.738 -18.538 83.08 -34.686 83.08 c --229.804 83.08 l --239.745 83.08 -247.804 75.021 -247.804 65.08 c --247.804 55.139 -239.745 47.08 -229.804 47.08 c --202.603 47.08 l --202.603 -25.345 l --202.603 -35.286 -194.543 -43.345 -184.603 -43.345 c --174.661 -43.345 -166.603 -35.286 -166.603 -25.345 c --166.603 47.08 l --108.077 47.08 l --108.077 -25.322 l --108.077 -35.264 -100.019 -43.322 -90.077 -43.322 c --80.137 -43.322 -72.077 -35.264 -72.077 -25.322 c --72.077 47.08 l --34.686 47.08 l --24.373 47.08 -19.426 44.298 -17.396 42.639 c --15.616 41.187 -14.714 39.526 -14.714 37.704 c --14.714 33.352 -20.137 26.447 -34.686 26.447 c --42.025 26.447 -48.63 21.991 -51.377 15.186 c --54.124 8.379 -52.465 0.587 -47.182 -4.509 c --12.171 -38.278 l --8.678 -41.647 -4.175 -43.322 0.323 -43.322 c -5.039 -43.322 9.749 -41.48 13.281 -37.818 c -20.183 -30.663 19.977 -19.269 12.821 -12.367 c -h -f -Q -0 G -8 w -q 1 0 0 1 257.9834 192.3545 cm -0 0 m -13.972 9.191 21.286 23.716 21.286 37.704 c -21.286 50.305 15.486 62.267 5.375 70.523 c --4.686 78.738 -18.538 83.08 -34.686 83.08 c --229.804 83.08 l --239.745 83.08 -247.804 75.021 -247.804 65.08 c --247.804 55.139 -239.745 47.08 -229.804 47.08 c --202.603 47.08 l --202.603 -25.345 l --202.603 -35.286 -194.543 -43.345 -184.603 -43.345 c --174.661 -43.345 -166.603 -35.286 -166.603 -25.345 c --166.603 47.08 l --108.077 47.08 l --108.077 -25.322 l --108.077 -35.264 -100.019 -43.322 -90.077 -43.322 c --80.137 -43.322 -72.077 -35.264 -72.077 -25.322 c --72.077 47.08 l --34.686 47.08 l --24.373 47.08 -19.426 44.298 -17.396 42.639 c --15.616 41.187 -14.714 39.526 -14.714 37.704 c --14.714 33.352 -20.137 26.447 -34.686 26.447 c --42.025 26.447 -48.63 21.991 -51.377 15.186 c --54.124 8.379 -52.465 0.587 -47.182 -4.509 c --12.171 -38.278 l --8.678 -41.647 -4.175 -43.322 0.323 -43.322 c -5.039 -43.322 9.749 -41.48 13.281 -37.818 c -20.183 -30.663 19.977 -19.269 12.821 -12.367 c -0 0 l -h -S -Q -/CS0 CS 1 1 1 SCN -60 w -q 1 0 0 1 257.9834 192.3545 cm -0 0 m -13.972 9.191 21.286 23.716 21.286 37.704 c -21.286 50.305 15.486 62.267 5.375 70.523 c --4.686 78.738 -18.538 83.08 -34.686 83.08 c --229.804 83.08 l --239.745 83.08 -247.804 75.021 -247.804 65.08 c --247.804 55.139 -239.745 47.08 -229.804 47.08 c --202.603 47.08 l --202.603 -25.345 l --202.603 -35.286 -194.543 -43.345 -184.603 -43.345 c --174.661 -43.345 -166.603 -35.286 -166.603 -25.345 c --166.603 47.08 l --108.077 47.08 l --108.077 -25.322 l --108.077 -35.264 -100.019 -43.322 -90.077 -43.322 c --80.137 -43.322 -72.077 -35.264 -72.077 -25.322 c --72.077 47.08 l --34.686 47.08 l --24.373 47.08 -19.426 44.298 -17.396 42.639 c --15.616 41.187 -14.714 39.526 -14.714 37.704 c --14.714 33.352 -20.137 26.447 -34.686 26.447 c --42.025 26.447 -48.63 21.991 -51.377 15.186 c --54.124 8.379 -52.465 0.587 -47.182 -4.509 c --12.171 -38.278 l --8.678 -41.647 -4.175 -43.322 0.323 -43.322 c -5.039 -43.322 9.749 -41.48 13.281 -37.818 c -20.183 -30.663 19.977 -19.269 12.821 -12.367 c -0 0 l -h -S -Q - -endstream endobj 48 0 obj <> endobj 35 0 obj [/ICCBased 39 0 R] endobj 27 0 obj [26 0 R] endobj 49 0 obj <> endobj xref -0 50 -0000000004 65535 f -0000000016 00000 n -0000000147 00000 n -0000015116 00000 n -0000000000 00000 f -0000015167 00000 n -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000017159 00000 n -0000000000 00000 f -0000017232 00000 n -0000017472 00000 n -0000019205 00000 n -0000084794 00000 n -0000150383 00000 n -0000215972 00000 n -0000281561 00000 n -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000314469 00000 n -0000320614 00000 n -0000015606 00000 n -0000016665 00000 n -0000311776 00000 n -0000311663 00000 n -0000314656 00000 n -0000307484 00000 n -0000016727 00000 n -0000320579 00000 n -0000306909 00000 n -0000306957 00000 n -0000311600 00000 n -0000311811 00000 n -0000314540 00000 n -0000314571 00000 n -0000314771 00000 n -0000314844 00000 n -0000314866 00000 n -0000315193 00000 n -0000315344 00000 n -0000315421 00000 n -0000320507 00000 n -0000320639 00000 n -trailer -<<52C01D6FCF06EF4AB4FAD4A3944F4582>]>> -startxref -320821 -%%EOF diff --git a/data/tr1/logo.png b/data/tr1/logo.png new file mode 100644 index 000000000..0d6c7cd52 Binary files /dev/null and b/data/tr1/logo.png differ diff --git a/data/tr1/ship/cfg/TR1X_gameflow.json5 b/data/tr1/ship/cfg/TR1X_gameflow.json5 index b338a778e..aa7898d49 100644 --- a/data/tr1/ship/cfg/TR1X_gameflow.json5 +++ b/data/tr1/ship/cfg/TR1X_gameflow.json5 @@ -8,12 +8,10 @@ "savegame_fmt_legacy": "saveati.%d", "savegame_fmt_bson": "save_tr1_%02d.dat", "demo_delay": 16, - "water_color": [0.45, 1.0, 1.0], - "draw_distance_fade": 22.0, - "draw_distance_max": 30.0, "injections": [ "data/injections/backpack.bin", "data/injections/braid.bin", + "data/injections/bubbles.bin", "data/injections/lara_animations.bin", "data/injections/purple_crystal.bin", "data/injections/uzi_sfx.bin", @@ -30,7 +28,7 @@ "music_track": 2, "inherit_injections": false, "sequence": [ - {"type": "display_picture", "path": "data/images/eidos.webp", "display_time": 1, "fade_in_time": 1.0, "fade_out_time": 1.0}, + {"type": "display_picture", "path": "data/images/eidos.webp", "legal": true, "display_time": 1, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "play_fmv", "fmv_id": 2}, @@ -59,6 +57,7 @@ "injections": [ "data/injections/lara_gym_guns.bin", "data/injections/braid.bin", + "data/injections/bubbles.bin", "data/injections/gym_textures.bin", "data/injections/lara_animations.bin", "data/injections/uzi_sfx.bin", @@ -151,6 +150,7 @@ "injections": [ "data/injections/folly_fd.bin", "data/injections/folly_itemrots.bin", + "data/injections/folly_pickup_meshes.bin", "data/injections/folly_textures.bin", ], }, @@ -246,6 +246,7 @@ "data/injections/khamoon_fd.bin", "data/injections/khamoon_mummy.bin", "data/injections/khamoon_textures.bin", + "data/injections/panther_sfx.bin", ], }, @@ -265,6 +266,7 @@ "data/injections/obelisk_meshfixes.bin", "data/injections/obelisk_skybox.bin", "data/injections/obelisk_textures.bin", + "data/injections/panther_sfx.bin", ], }, @@ -306,6 +308,7 @@ "data/injections/mines_meshfixes.bin", "data/injections/mines_pushblocks.bin", "data/injections/mines_textures.bin", + "data/injections/skate_kid_sfx.bin", ], "item_drops": [ {"enemy_num": 17, "object_ids": [86]}, @@ -481,8 +484,8 @@ { "path": "data/cut4.phd", "music_track": 22, - "draw_distance_fade": 12.0, - "draw_distance_max": 18.0, + "fog_start": 12.0, + "fog_end": 18.0, "lara_type": "player_1", "inherit_injections": false, "injections": [ @@ -503,8 +506,8 @@ // FMVs "fmvs": [ - {"path": "fmv/core.avi"}, - {"path": "fmv/escape.avi"}, + {"path": "fmv/core.avi", "legal": true}, + {"path": "fmv/escape.avi", "legal": true}, {"path": "fmv/cafe.avi"}, {"path": "fmv/mansion.avi"}, {"path": "fmv/snow.avi"}, diff --git a/data/tr1/ship/cfg/TR1X_gameflow_demo_pc.json5 b/data/tr1/ship/cfg/TR1X_gameflow_demo_pc.json5 index cbe21e47a..743012b96 100644 --- a/data/tr1/ship/cfg/TR1X_gameflow_demo_pc.json5 +++ b/data/tr1/ship/cfg/TR1X_gameflow_demo_pc.json5 @@ -9,12 +9,10 @@ "savegame_fmt_legacy": "save_demo_pc.%d", "savegame_fmt_bson": "save_demo_pc_%02d.dat", "demo_delay": 16, - "water_color": [0.45, 1.0, 1.0], - "draw_distance_fade": 22.0, - "draw_distance_max": 30.0, "injections": [ "data/injections/backpack.bin", "data/injections/braid.bin", + "data/injections/bubbles.bin", "data/injections/lara_animations.bin", "data/injections/lara_jumping.bin", "data/injections/uzi_sfx.bin", diff --git a/data/tr1/ship/cfg/TR1X_gameflow_level.json5 b/data/tr1/ship/cfg/TR1X_gameflow_level.json5 index 95a236920..035285740 100644 --- a/data/tr1/ship/cfg/TR1X_gameflow_level.json5 +++ b/data/tr1/ship/cfg/TR1X_gameflow_level.json5 @@ -5,12 +5,10 @@ "savegame_fmt_legacy": "save_tmp.%d", "savegame_fmt_bson": "save_tmp_%02d.dat", "demo_delay": 0, - "water_color": [0.45, 1.0, 1.0], - "draw_distance_fade": 22.0, - "draw_distance_max": 30.0, "injections": [ "data/injections/backpack.bin", "data/injections/braid.bin", + "data/injections/bubbles.bin", "data/injections/lara_animations.bin", "data/injections/lara_jumping.bin", "data/injections/uzi_sfx.bin", diff --git a/data/tr1/ship/cfg/TR1X_gameflow_ub.json5 b/data/tr1/ship/cfg/TR1X_gameflow_ub.json5 index 9817d0510..fab10e0ff 100644 --- a/data/tr1/ship/cfg/TR1X_gameflow_ub.json5 +++ b/data/tr1/ship/cfg/TR1X_gameflow_ub.json5 @@ -8,12 +8,10 @@ "savegame_fmt_legacy": "saveuba.%d", "savegame_fmt_bson": "save_trub_%02d.dat", "demo_delay": 16, - "water_color": [0.45, 1.0, 1.0], - "draw_distance_fade": 22, - "draw_distance_max": 30, "injections": [ "data/injections/backpack.bin", "data/injections/braid.bin", + "data/injections/bubbles.bin", "data/injections/lara_animations.bin", "data/injections/uzi_sfx.bin", "data/injections/explosion.bin", @@ -36,7 +34,7 @@ "data/injections/pda_model.bin", ], "sequence": [ - {"type": "display_picture", "path": "data/images/eidos.webp", "display_time": 1, "fade_in_time": 1.0, "fade_out_time": 1.0}, + {"type": "display_picture", "path": "data/images/eidos.webp", "legal": true, "display_time": 1, "fade_in_time": 1.0, "fade_out_time": 1.0}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, @@ -44,7 +42,7 @@ }, "levels": [ - // Level 0: Return to Egypt + // Level 1: Return to Egypt { "path": "data/egypt.phd", "music_track": 59, @@ -58,11 +56,12 @@ "data/injections/egypt_fd.bin", "data/injections/egypt_meshfixes.bin", "data/injections/egypt_textures.bin", + "data/injections/panther_sfx.bin", ], "unobtainable_kills": 1, }, - // Level 1: Temple of the Cat + // Level 2: Temple of the Cat { "path": "data/cat.phd", "music_track": 59, @@ -77,11 +76,12 @@ "data/injections/cat_itemrots.bin", "data/injections/cat_meshfixes.bin", "data/injections/cat_textures.bin", + "data/injections/panther_sfx.bin", ], "unobtainable_pickups": 1, }, - // Level 2: Atlantean Stronghold + // Level 3: Atlantean Stronghold { "path": "data/end.phd", "music_track": 60, @@ -98,7 +98,7 @@ "unobtainable_kills": 1, }, - // Level 3: The Hive + // Level 4: The Hive { "path": "data/end2.phd", "music_track": 60, @@ -123,7 +123,8 @@ {"type": "dummy"}, - // Level 5: Current Position + // Level 6: Current Position + // This level is necessary to read TombATI's save files. { "path": "data/current.phd", "type": "current", @@ -134,7 +135,7 @@ ], "fmvs": [ - {"path": "fmv/core.avi"}, - {"path": "fmv/escape.avi"}, + {"path": "fmv/core.avi", "legal": true}, + {"path": "fmv/escape.avi", "legal": true}, ], } diff --git a/data/tr1/ship/cfg/TR1X_strings.json5 b/data/tr1/ship/cfg/TR1X_strings.json5 index 43e604269..a429562bc 100644 --- a/data/tr1/ship/cfg/TR1X_strings.json5 +++ b/data/tr1/ship/cfg/TR1X_strings.json5 @@ -271,7 +271,7 @@ "door_8": {"name": "Door 8"}, "trapdoor_1": {"name": "Trapdoor 1"}, "trapdoor_2": {"name": "Trapdoor 2"}, - "big_trapdoor": {"name": "Big Trapdoor"}, + "trapdoor_3": {"name": "Trapdoor 3"}, "bridge_flat": {"name": "Bridge Flat"}, "bridge_tilt_1": {"name": "Bridge Tilt 1"}, "bridge_tilt_2": {"name": "Bridge Tilt 2"}, @@ -341,35 +341,39 @@ }, "game_strings": { - "CONTROL_BACKEND_CONTROLLER": "Controller", - "CONTROL_BACKEND_KEYBOARD": "Keyboard", - "CONTROL_CUSTOMIZE": "Customize Controls", - "CONTROL_CUSTOM_1": "User Keys 1", - "CONTROL_CUSTOM_2": "User Keys 2", - "CONTROL_CUSTOM_3": "User Keys 3", - "CONTROL_DEFAULT_KEYS": "Default Keys", - "CONTROL_RESET_DEFAULTS": "Reset All: Hold %s", - "CONTROL_UNBIND": "Unbind: Hold %s", + "CONTROLS_BACKEND_CONTROLLER": "Controller", + "CONTROLS_BACKEND_KEYBOARD": "Keyboard", + "CONTROLS_CUSTOMIZE": "Customize Controls", + "CONTROLS_CUSTOM_1": "User Keys 1", + "CONTROLS_CUSTOM_2": "User Keys 2", + "CONTROLS_CUSTOM_3": "User Keys 3", + "CONTROLS_DEFAULT_KEYS": "Default Keys", + "CONTROLS_RESET_DEFAULTS": "Reset All: Hold %s", + "CONTROLS_UNBIND": "Unbind: Hold %s", "DETAIL_BILINEAR": "Bilinear", "DETAIL_BRIGHTNESS": "Brightness", - "DETAIL_DECIMAL_FMT": "%d", "DETAIL_FBO_FILTER": "FBO filter", "DETAIL_FLOAT_FMT": "%.1f", + "DETAIL_FOG_END": "Fog end", + "DETAIL_FOG_START": "Fog start", "DETAIL_FPS": "FPS", - "DETAIL_PRETTY_PIXELS": "Pretty pixels", + "DETAIL_INTEGER_FMT": "%d", "DETAIL_REFLECTIONS": "Reflections", "DETAIL_RENDER_MODE": "Render mode", "DETAIL_RENDER_MODE_FBO": "Framebuffer", "DETAIL_RENDER_MODE_LEGACY": "Window size", "DETAIL_RESOLUTION": "Resolution", "DETAIL_RESOLUTION_FMT": "%dx%d", - "DETAIL_SELECT_DETAIL": "Select Detail", "DETAIL_STRING_FMT": "%s", "DETAIL_TEXTURE_FILTER": "Texture filter", + "DETAIL_TITLE": "Graphic Options", "DETAIL_TRAPEZOID_FILTER": "Trapezoid filter", "DETAIL_UI_BAR_SCALE": "UI bar scale", "DETAIL_UI_TEXT_SCALE": "UI text scale", "DETAIL_VSYNC": "VSync", + "DETAIL_WATER_COLOR_B": "Water color (B)", + "DETAIL_WATER_COLOR_G": "Water color (G)", + "DETAIL_WATER_COLOR_R": "Water color (R)", "HEADING_GAME_OVER": "GAME OVER", "HEADING_INVENTORY": "INVENTORY", "HEADING_ITEMS": "ITEMS", diff --git a/data/tr1/ship/cfg/TR1X_strings_ub.json5 b/data/tr1/ship/cfg/TR1X_strings_ub.json5 index 1a704f6cd..36e1134cb 100644 --- a/data/tr1/ship/cfg/TR1X_strings_ub.json5 +++ b/data/tr1/ship/cfg/TR1X_strings_ub.json5 @@ -1,6 +1,6 @@ { "levels": [ - // Level 0: Return to Egypt + // Level 1: Return to Egypt { "title": "Return to Egypt", "objects": { @@ -8,7 +8,7 @@ }, }, - // Level 1: Temple of the Cat + // Level 2: Temple of the Cat { "title": "Temple of the Cat", "objects": { @@ -16,22 +16,22 @@ }, }, - // Level 2: Atlantean Stronghold + // Level 3: Atlantean Stronghold { "title": "Atlantean Stronghold", }, - // Level 3: The Hive + // Level 4: The Hive { "title": "The Hive", }, - // Level 4: Title + // Level 5: Title { "title": "Title", }, - // Level 5: Current Position + // Level 6: Current Position { "title": "Current Position", }, diff --git a/data/tr1/ship/data/images/atlantis.webp b/data/tr1/ship/data/images/atlantis.webp deleted file mode 100644 index 1387e1f12..000000000 Binary files a/data/tr1/ship/data/images/atlantis.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/credits_1.webp b/data/tr1/ship/data/images/credits_1.webp deleted file mode 100644 index 174e94bb2..000000000 Binary files a/data/tr1/ship/data/images/credits_1.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/credits_2.webp b/data/tr1/ship/data/images/credits_2.webp deleted file mode 100644 index 67f5a1b70..000000000 Binary files a/data/tr1/ship/data/images/credits_2.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/credits_3.webp b/data/tr1/ship/data/images/credits_3.webp deleted file mode 100644 index c10913e67..000000000 Binary files a/data/tr1/ship/data/images/credits_3.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/credits_3_alt.webp b/data/tr1/ship/data/images/credits_3_alt.webp deleted file mode 100644 index 868dde66d..000000000 Binary files a/data/tr1/ship/data/images/credits_3_alt.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/credits_ps1.webp b/data/tr1/ship/data/images/credits_ps1.webp deleted file mode 100644 index c7d81d416..000000000 Binary files a/data/tr1/ship/data/images/credits_ps1.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/credits_ub.webp b/data/tr1/ship/data/images/credits_ub.webp deleted file mode 100644 index 69cc8824e..000000000 Binary files a/data/tr1/ship/data/images/credits_ub.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/egypt.webp b/data/tr1/ship/data/images/egypt.webp deleted file mode 100644 index f3b2c66ce..000000000 Binary files a/data/tr1/ship/data/images/egypt.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/eidos.webp b/data/tr1/ship/data/images/eidos.webp deleted file mode 100644 index 5439e9103..000000000 Binary files a/data/tr1/ship/data/images/eidos.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/end.webp b/data/tr1/ship/data/images/end.webp deleted file mode 100644 index e5b858de9..000000000 Binary files a/data/tr1/ship/data/images/end.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/greece.webp b/data/tr1/ship/data/images/greece.webp deleted file mode 100644 index 060bd0291..000000000 Binary files a/data/tr1/ship/data/images/greece.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/greece_saturn.webp b/data/tr1/ship/data/images/greece_saturn.webp deleted file mode 100644 index 618bc6565..000000000 Binary files a/data/tr1/ship/data/images/greece_saturn.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/gym.webp b/data/tr1/ship/data/images/gym.webp deleted file mode 100644 index 9e026fcc7..000000000 Binary files a/data/tr1/ship/data/images/gym.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/install.webp b/data/tr1/ship/data/images/install.webp deleted file mode 100644 index f79c61dfc..000000000 Binary files a/data/tr1/ship/data/images/install.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/peru.webp b/data/tr1/ship/data/images/peru.webp deleted file mode 100644 index 4a3f79fc3..000000000 Binary files a/data/tr1/ship/data/images/peru.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/title.webp b/data/tr1/ship/data/images/title.webp deleted file mode 100644 index a94f8a959..000000000 Binary files a/data/tr1/ship/data/images/title.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/title_og_alt.webp b/data/tr1/ship/data/images/title_og_alt.webp deleted file mode 100644 index 723256902..000000000 Binary files a/data/tr1/ship/data/images/title_og_alt.webp and /dev/null differ diff --git a/data/tr1/ship/data/images/title_ub.webp b/data/tr1/ship/data/images/title_ub.webp deleted file mode 100644 index 7e269f718..000000000 Binary files a/data/tr1/ship/data/images/title_ub.webp and /dev/null differ diff --git a/data/tr1/ship/data/injections/bubbles.bin b/data/tr1/ship/data/injections/bubbles.bin new file mode 100644 index 000000000..c399a73f8 Binary files /dev/null and b/data/tr1/ship/data/injections/bubbles.bin differ diff --git a/data/tr1/ship/data/injections/folly_pickup_meshes.bin b/data/tr1/ship/data/injections/folly_pickup_meshes.bin new file mode 100644 index 000000000..044f6717e Binary files /dev/null and b/data/tr1/ship/data/injections/folly_pickup_meshes.bin differ diff --git a/data/tr1/ship/data/injections/panther_sfx.bin b/data/tr1/ship/data/injections/panther_sfx.bin new file mode 100644 index 000000000..199c2e833 Binary files /dev/null and b/data/tr1/ship/data/injections/panther_sfx.bin differ diff --git a/data/tr1/ship/data/injections/skate_kid_sfx.bin b/data/tr1/ship/data/injections/skate_kid_sfx.bin new file mode 100644 index 000000000..6227d7485 Binary files /dev/null and b/data/tr1/ship/data/injections/skate_kid_sfx.bin differ diff --git a/data/tr1/ship/shaders/common.glsl b/data/tr1/ship/shaders/common.glsl index b3845ded8..0cf4d9e7d 100644 --- a/data/tr1/ship/shaders/common.glsl +++ b/data/tr1/ship/shaders/common.glsl @@ -1,15 +1,26 @@ +#define WALL_L 1024 +#define SHADE_NEUTRAL 0x1000 +#define SHADE_MAX 0x1FFF + +#define VERT_NO_CAUSTICS 0x01 +#define VERT_FLAT_SHADED 0x02 +#define VERT_REFLECTIVE 0x04 +#define VERT_NO_LIGHTING 0x08 +#define VERT_SPRITE 0x10 // flag for billboarded sprites in mesh shader + +#define WIBBLE_SIZE 32 +#define MAX_WIBBLE 2 #define PI 3.1415926538 -vec3 waterWibble(vec4 position, vec2 viewportSize, float wibbleOffset) +vec3 waterWibble(vec4 position, vec2 viewportSize, int time) { // get screen coordinates vec3 ndc = position.xyz / position.w; //perspective divide/normalize vec2 viewportCoord = ndc.xy * 0.5 + 0.5; //ndc is -1 to 1 in GL. scale for 0 to 1 vec2 viewportPixelCoord = viewportCoord * viewportSize; - float amplitude = 2.0; - viewportPixelCoord.x += sin((wibbleOffset + viewportPixelCoord.y) * 2.0 * PI / 32) * amplitude; - viewportPixelCoord.y += sin((wibbleOffset + viewportPixelCoord.x) * 2.0 * PI / 32) * amplitude; + viewportPixelCoord.x += sin((float(time) + viewportPixelCoord.y) * 2.0 * PI / WIBBLE_SIZE) * MAX_WIBBLE; + viewportPixelCoord.y += sin((float(time) + viewportPixelCoord.x) * 2.0 * PI / WIBBLE_SIZE) * MAX_WIBBLE; // reverse transform viewportCoord = viewportPixelCoord / viewportSize; @@ -17,6 +28,24 @@ vec3 waterWibble(vec4 position, vec2 viewportSize, float wibbleOffset) return ndc * position.w; } +float shadeFog(float shade, float depth, vec2 fog) +{ + float fogBegin = fog.x; + float fogEnd = fog.y; + if (depth < fogBegin) { + return shade + 0.0; + } else if (depth >= fogEnd) { + return shade + float(SHADE_MAX); + } else { + return shade + (depth - fogBegin) * SHADE_MAX / (fogEnd - fogBegin); + } +} + +vec3 applyShade(vec3 color, float shade) +{ + return color * (2.0 - (shade / SHADE_NEUTRAL)); +} + bool discardTranslucent(sampler2D tex, vec2 uv) { // do not use smoothing for chroma key @@ -26,11 +55,11 @@ bool discardTranslucent(sampler2D tex, vec2 uv) return texel.a == 0.0; } -bool discardTranslucent(sampler2DArray tex, vec3 uv) +bool discardTranslucent(sampler2DArray tex, vec3 uvw) { // do not use smoothing for chroma key ivec2 size = textureSize(tex, 0).xy; - ivec3 texCoordsNN = ivec3(ivec2(uv.xy * size.xy) % size.xy, uv.z); + ivec3 texCoordsNN = ivec3(ivec2(uvw.xy * size.xy) % size.xy, uvw.z); vec4 texel = texelFetch(tex, texCoordsNN, 0); return texel.a == 0.0; } diff --git a/data/tr1/ship/shaders/meshes.glsl b/data/tr1/ship/shaders/meshes.glsl new file mode 100644 index 000000000..ea17d37d5 --- /dev/null +++ b/data/tr1/ship/shaders/meshes.glsl @@ -0,0 +1,122 @@ +#ifdef VERTEX + +uniform int uTime; +uniform vec2 uViewportSize; +uniform mat4 uMatProjection; +uniform mat4 uMatModelView; +uniform bool uTrapezoidFilterEnabled; +uniform bool uWibbleEffect; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec3 inUVW; +layout(location = 3) in vec4 inTextureSize; +layout(location = 4) in vec2 inTrapezoidRatios; +layout(location = 5) in int inFlags; +layout(location = 6) in vec4 inColor; +layout(location = 7) in float inShade; + +out vec4 gWorldPos; +out vec3 gNormal; +flat out int gFlags; +flat out int gTexLayer; +out vec2 gTexUV; +flat out vec4 gAtlasSize; +out vec2 gTrapezoidRatios; +out float gShade; +out vec4 gColor; + +void main(void) { + // billboard sprites if flagged, else standard vertex transform + vec4 eyePos = uMatModelView * vec4(inPosition.xyz, 1.0); + if ((inFlags & VERT_SPRITE) != 0) { + // inNormal.xy carries sprite displacement for billboarding + eyePos.xy += inNormal.xy; + } + gWorldPos = eyePos; + gNormal = inNormal; + gl_Position = uMatProjection * eyePos; + + // apply water wibble effect only to non-sprite vertices + if (uWibbleEffect && (inFlags & VERT_NO_CAUSTICS) == 0 && (inFlags & VERT_SPRITE) == 0) { + gl_Position.xyz = + waterWibble(gl_Position, uViewportSize, uTime); + } + + gFlags = inFlags; + gAtlasSize = inTextureSize; + gTexUV = inUVW.xy; + gTexLayer = int(inUVW.z); + gTrapezoidRatios = inTrapezoidRatios; + if (uTrapezoidFilterEnabled) { + gTexUV *= inTrapezoidRatios; + } + gShade = inShade; + gColor = inColor; +} + +#elif defined(FRAGMENT) + +uniform int uTime; +uniform sampler2DArray uTexAtlas; +uniform sampler2D uTexEnvMap; +uniform bool uSmoothingEnabled; +uniform bool uAlphaDiscardEnabled; +uniform bool uTrapezoidFilterEnabled; +uniform bool uReflectionsEnabled; +uniform float uAlphaThreshold; +uniform float uBrightnessMultiplier; +uniform vec3 uGlobalTint; +uniform vec2 uFog; // x = fog start, y = fog end +uniform bool uWaterEffect; + +in vec4 gWorldPos; +in vec3 gNormal; +flat in int gFlags; +flat in int gTexLayer; +in vec2 gTexUV; +flat in vec4 gAtlasSize; +in float gShade; +in vec4 gColor; +in vec2 gTrapezoidRatios; +out vec4 outColor; + +vec2 clampTexAtlas(vec2 uv, vec4 atlasSize) +{ + float epsilon = 0.5 / 256.0; + return clamp(uv, atlasSize.xy + epsilon, atlasSize.zw - epsilon); +} + +void main(void) { + vec4 texColor = gColor; + vec3 texCoords = vec3(gTexUV.x, gTexUV.y, gTexLayer); + if (uTrapezoidFilterEnabled) { + texCoords.xy /= gTrapezoidRatios; + } + texCoords.xy = clampTexAtlas(texCoords.xy, gAtlasSize); + + if ((gFlags & VERT_FLAT_SHADED) == 0 && texCoords.z >= 0) { + if (uAlphaDiscardEnabled && uSmoothingEnabled && discardTranslucent(uTexAtlas, texCoords)) { + discard; + } + + texColor = texture(uTexAtlas, texCoords); + if (uAlphaThreshold >= 0.0 && texColor.a <= uAlphaThreshold) { + discard; + } + } + if ((gFlags & VERT_REFLECTIVE) != 0 && uReflectionsEnabled) { + texColor *= texture(uTexEnvMap, (normalize(gNormal) * 0.5 + 0.5).xy) * 2; + } + + if ((gFlags & VERT_NO_LIGHTING) == 0) { + float shade = gShade; + shade = shadeFog(shade, gWorldPos.z, uFog); + texColor.rgb = applyShade(texColor.rgb, shade); + texColor.rgb *= uGlobalTint; + } + + texColor.rgb *= uBrightnessMultiplier; + outColor = vec4(texColor.rgb, gColor.a); +} +#endif diff --git a/data/tr1/ship/shaders/sprites.glsl b/data/tr1/ship/shaders/sprites.glsl deleted file mode 100644 index 2f5334e2b..000000000 --- a/data/tr1/ship/shaders/sprites.glsl +++ /dev/null @@ -1,88 +0,0 @@ -#define NEW_ZBUFFER 0 -#define NEUTRAL_SHADE 0x1000 - -#ifdef VERTEX - -uniform samplerBuffer uUVW; // texture u, v, layer -uniform vec2 uViewportCenter; -uniform vec2 uViewportSize; -uniform mat4 uMatProjection; -uniform mat4 uMatProjectionOG; -uniform mat4 uMatModelView; -uniform float uWibbleOffset; -uniform vec2 uFog; // x = start, y = end - -uniform float uPhdPersp; -uniform float uPhdResZ; -uniform float uPhdResZBuf; - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec2 inDisplacement; -layout(location = 2) in int inTextureIdx; -layout(location = 3) in float inShade; - -out vec3 gUV; // x = u, y = v, z = layer -out float gShade; - -void main(void) { - #if NEW_ZBUFFER - vec4 centerEyeSpace = uMatModelView * vec4(inPosition, 1.0); - centerEyeSpace.xy += inDisplacement; - gl_Position = uMatProjection * centerEyeSpace; - - if (uWibbleOffset >= 0.0) { - gl_Position.xyz = - waterWibble(gl_Position, uViewportSize, uWibbleOffset); - } - #else - vec3 localPos = inPosition; - vec3 worldPos = (uMatModelView * vec4(localPos, 1)).xyz; - - if ((uMatProjection * vec4(worldPos, 1)).z <= 0) { - // Terrible hack: - // Push the vertex out of view (e.g. offscreen or behind clip). - // Works for entire sprites, because worldPos is the position of the - // sprite origin, the same for all vertices. - gl_Position = vec4(2.0, 2.0, 1.0, 1.0); - return; - } - - vec3 screenCenterPos = vec3( - uViewportCenter + worldPos.xy * uPhdPersp / worldPos.z, - uPhdResZBuf - uPhdResZ / worldPos.z); - vec3 screenCornerPos = screenCenterPos; - screenCornerPos.xy += inDisplacement * uPhdPersp / worldPos.z; - gl_Position = (uMatProjectionOG * vec4(screenCornerPos, 1)); - #endif - - gUV = texelFetch(uUVW, int(inTextureIdx)).xyz; - gShade = inShade; -} - -#elif defined(FRAGMENT) - -uniform sampler2DArray uTexture; -uniform bool uSmoothingEnabled; -uniform float uBrightnessMultiplier; -uniform vec3 uGlobalTint; - -in vec3 gUV; -in float gShade; -out vec4 outColor; - -void main(void) { - vec4 texColor = texture(uTexture, gUV); - if (uSmoothingEnabled && discardTranslucent(uTexture, gUV)) { - discard; - } - - if (texColor.a <= 0.0) { - discard; - } - - texColor.rgb *= 2.0 - (gShade / NEUTRAL_SHADE); - texColor.rgb *= uBrightnessMultiplier; - texColor.rgb *= uGlobalTint; - outColor = vec4(texColor.rgb, 1.0); -} -#endif diff --git a/data/tr2/logo-dark-theme.png b/data/tr2/logo-dark-theme.png deleted file mode 100644 index 16c9fbc76..000000000 Binary files a/data/tr2/logo-dark-theme.png and /dev/null differ diff --git a/data/tr2/logo-light-theme.png b/data/tr2/logo-light-theme.png deleted file mode 100644 index fe050d2ed..000000000 Binary files a/data/tr2/logo-light-theme.png and /dev/null differ diff --git a/data/tr2/logo.ai b/data/tr2/logo.ai deleted file mode 100644 index 3791eaea4..000000000 --- a/data/tr2/logo.ai +++ /dev/null @@ -1,1489 +0,0 @@ -%PDF-1.6 % -1 0 obj <>/OCGs[24 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - logo - - - 2023-09-25T22:02:23+02:00 - 2023-09-25T22:02:23+02:00 - 2023-09-25T22:02:23+02:00 - Adobe Illustrator 27.9 (Windows) - - - - 256 - 104 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAaAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4qoDnMxoeKDChv6sP5 jja076sP5jja076sP5jja076sP5jja076sP5jja076sP5jja076sP5jja076sP5jja076sP5jja0 76sP5jja076t4ORja01xuE3Dch4YqvjmD7HZvDAlUxVa8ioKn6BiqkGnk+z8K4UN/V2P2nJxtad9 W/yzja01wnTdW5DwOKr4pg+xFG8MCqjEAEnoMUodVkm+JmovYYUNkSQmoPJO4xVWR1ZajpgS3iqn LLw2G7noMULBbs27seWG1ptGeOQRsag9DiqtgS7FXEVBHjiqhA4SqNsQcJQFbkviMCWwwPQ4q7FW uS+IxV3JfEYq7kviMVbBB6HFXYq1yXxGKu5L4jFW+SnuMVdiqlPHUc12Yb4Qgr435IG+/AlSjHqy F2+yOgwoV8CXEgddsVcCD0NcVdiqjcLSkg2I64QgqjfHFt+0NsCVlu448Dsw7YSgKvXAlQZWhbku 6HqMKFz3ChRx3Y9BjS26KIj433c/hiqrgSoE+pOvHovU4UK+BLsVdiq14kfqN/HFVn1aP3w2ilio EuAB0p/DFURgShxGrzOD2woX/Vo/fG1p31aP3xtaWxqFuCo6AYqr4EodY1eZwexwoX/Vo/fG1ppr eIDqR742tOt2Y8lrUDocSoVsCVCCpjcD6PuwlAWxy+mvEqa1xVf9ZH8pxpbWsxlcVBCDrirduP3j FfsYlQr4EqdwQIiPHCEFdGKIo9sCVksXL4l2cYUNxS8vhbZx1GBVTrilYkKKxYfR7YrS/FVB3aRv Tj6dzhQqxxqi0H0nAldirsVdirsVdiqif96h8v4YUK2BKjH/AH74UK2BLsVUV/3qb5fwGFCtgSox f30mFDpGd5PTU0A6nFXC1TuScbWlVVVRRRQYErZnCIT3OwxVqBOMYr1O+JUKmKtM6qKsaYqo/HMf 5Y8KFZVCig6YEud1QVOKqKK0r822QdBhQr4EuxVQuOIYEbSe2EIKsnLiOX2u+BLeKqVwX47fZ/aw hBXQhOA4/T41wJC/FXYq7FXYq7FXYqon/eofL+GFCtgSox/374UK2BLsVUV/3qb5fwGFCtgSoxf3 0mFDVfTnPLo3fFVfAlxrQ03PbFUMn7yX94dx0XChE4EqckwU8V+JvDCi1qwsx5Smp/lxWlYAAUHT AlZJMqbdW8MVUFZHblK3yXfChXWaI7Bv4YEr8VU5ZeAoN2PQYq1FEQeb7ufwwoVcCVqyozFQdxiq 7FVBlaJuafZ7jChWR1dajAlvFXYq7FXYq7FVE/71D5fwwoVsCVGP+/fChWwJdiqiv+9TfL+AwoVs CVGL++k/z74UKrorrQ4EqKu0TcH3XscKFfAlSnjBHMbMu9cKC4SH0OfemKut0AQN1Y98SoVcCXYq t9KP+UYq70o/5Riq14I2GwoexGNopbDIfSYncrhV0CV/eNux6YlQrYEqEkjO3px/ScKG2twFHA/G O/jja0uil5fC2zjFVTAlQI9KZeP2X7YUK+BLsVdirsVdiqjMrK4kUVp1GFDvrSU2Br4Y0ttwI27t 1btiVCrgS7FVGUMkglUVHcYUONytNga+GNLbcCMAWb7TYlQq4EtOiuKHFVAM8J4tunY4UNvNzHBA anritqixgRcD4b4EqSSGL4HG3YjChf8AWY/fGlt31mP3xpbd9Zj98aW3fWY/fGltprgEUQEsemNL a6OKkRU9W64FU0kMXwONuxGFW2laX4IwQO5xVVjjVFoPpOBK7FVOWLl8S7OMKFq3FBRwQwxpbaXl LIHIoi9MVV8CXYq7FXYq7FXYq6grWm+KuxV2KuxV2KuoOtMVdirsVcSBudsVWGWLoWGKrlKkfDSn tireKuIB64q1xXwGKu4r4DFXcV8BiruK+AxVsADoKYq7FXEA9cVcAB0xV2KuxV2KuIB64q7FXYq7 FXFgOpA+eKrfUj/mH3jFXepH/MPvxVdirsVdirsVdirRZR1IGKtGWMftDFVpni/m/XjS2ta4B2QV Y4aRax0pRpiST0UYq2ptj1FPnXFW/RH2omofnitNrOynjKKHxxW1aopWu3jgS0GU9CD8sVbxV2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KqEv7yZU7DrhQv8Aq8Xh+ONrSlIsStxVSW9jircbzqvEIT4V xVsvc+AH+fzxVvjcn9oDFXelMesn3YqtMSVo0u+KrhbRUrUkY2tNpFARUCtPniqz1IQdo6jxxVUj kib7Ox8OmBW5Yw607jocUqQdR8Ey7j9rChqIAzVjrxHXFVd1Rlo3TAlQ9OEH4pKjsBhQ5GjSaqn4 KYqrevF/NgTbvXi/mxW3evF/Nitu9eL+bFbd68X82K2714v5sVt3rxfzYrbvXi/mxW3evF/Nitu9 eL+bFbd68X82K2714v5sVt3rxfzYrbvWi/mxW1ODaRw32sJQFfAlRTa5avcbfhhQ3IZkfkPiXwxV YFWZySGGKrkSRH4g1Q/hirXGWInj8SntirUaKzEGMgeNTiq4RyRuOHxIeoxVzQsrcojTxGKtB5+g Sh8cVXvCHFacX8RitLow4Qc+uBK4qD1FfniqHrKZDGCF+W2FC4WwO7sScbWl4giH7OBNO9GL+UYq 70Yv5RirvRi/lGKu9GL+UYq70Yv5RirvRi/lGKu9GL+UYq70Yv5RirvRi/lGKu9GL+UYq70Yv5Ri rvRi/lGKu9GL+UYqsk9BP2QW8MKGoYSW5sKeC4q6b4JFkHTocVb+tR+B/DGltTkkRm5KSDTw/txV ckMjLUuV9sVXejL/AL8P44rTvRl/34fxxWnelN/vzFXelP8A78xV3pz/AM+Ku9O4/nH+f0Yq7hc/ zj/P6MVdS6HcH7sVd6sy/aSvyxVtbmM9ajGltqVQ4DxmrL4YquSdGHxGjd8aW13qx/zDAl3qx/zD FXerH/MMVd6sf8wxV3qx/wAwxV3qx/zDFXerH/MMVd6sf8wxV3qx/wAwxV3qx/zDFWvWi/mxVo3E Q7k/RhpFrfrBP2EJxpbdxuH6ngMVXJAib9T4nAmlTFXYq6g8MVdQeGKuxV2KuxV2KuxV2KuxV2Ku xV2KtMit1AOKqTW9DWM8ThtFLGSStWQN7j+zFWjGzbCPj77/AMcVVlt4wBUVPc742tO9CL+X8Tgt NO9CL+X8Tja070Iv5fxONrTvQi/l/E42tO9CL+X8Tja070Iv5fxONrTvQi/l/E42tO9CL+X8Tja0 36MX8uK0uEaDoo+7FW8VdirsVdir/9k= - - - - uuid:9d823f4c-4b4b-4823-ae38-c33bf13c4295 - xmp.did:c75b5b7c-47a6-ef43-8012-54e10b5ca555 - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - uuid:85b06774-401c-479c-8001-7d3b0b129933 - xmp.did:0fb69b92-9296-f643-8766-fd53738d52f9 - uuid:5D20892493BFDB11914A8590D31508C8 - default - - - - - saved - xmp.iid:0fb69b92-9296-f643-8766-fd53738d52f9 - 2023-09-25T20:39:44+02:00 - Adobe Illustrator 27.9 (Windows) - / - - - saved - xmp.iid:c75b5b7c-47a6-ef43-8012-54e10b5ca555 - 2023-09-25T21:59:35+02:00 - Adobe Illustrator 27.9 (Windows) - / - - - - Document - Print - AIRobin - False - False - 1 - - 625.000000 - 250.000000 - Pixels - - - - Cyan - Magenta - Yellow - - - - - - Default Swatch Group - 0 - - - - C=4 M=2 Y=1 K=0 1 - RGB - PROCESS - 241 - 244 - 246 - - - White - RGB - PROCESS - 255 - 255 - 255 - - - Black - RGB - PROCESS - 35 - 31 - 32 - - - - - - Grays - 1 - - - - C=0 M=0 Y=0 K=100 - RGB - PROCESS - 35 - 31 - 32 - - - - - - - Adobe PDF library 17.00 - - - - - - - - - - - - - - - - - - - - - - - - - -endstream endobj 3 0 obj <> endobj 5 0 obj <>/ExtGState<>/Properties<>>>/Thumb 29 0 R/TrimBox[0.0 0.0 625.0 250.0]/Type/Page/PieceInfo<>>> endobj 26 0 obj <>stream -HtIEuKgF[ BxF$#nGjETdd  K -}?Э~Kx1!UcGd~3ˡWNr'ӡWKm?/+^CpE+[ 'gVbͥE^=SkaRR̙Rq]X.(ks : 0cS)Yt#Gm1抩,#vfg\'pKc[5ơ}e}.!z6l-S42 BѦ"uˏ݋+V["{#oD8GKY-ĚaL`Oy]ku1aEeq@1Gbl𰭸F~B YpYh 92[88閰JbHcŐMԬ(Cqg$e?)dVܰSMX~нF(oɹrf7{[`J@$DzVB|e5fAvg/)Xodw: K^Ҩ/<)j`6i&'O%6-7Q -ц;rmumF'+- -M99zj[:4Ro]vNnu} y4μu9WZN\qiy0-'43_|~8= -endstream endobj 29 0 obj <>stream -8;ZDm9+HIp$q0Q6Qe??qY%%kUBPmQE#ReZi85U4q[)TUZBT@eEa4i\9f/'&[RRDHl -NjX&<7Nq4?7_a+8n'pa#`t4f+\/0!^a_X1)b@]V*V[/!!!$!rr<$!s8N0$XN0%'~> -endstream endobj 8 0 obj <> endobj 9 0 obj <> endobj 10 0 obj <>stream -%!PS-Adobe-3.0 -%%Creator: Adobe Illustrator(R) 24.0 -%%AI8_CreatorVersion: 27.9.0 -%%For: (Marcin Kurczewski) () -%%Title: (logo.ai) -%%CreationDate: 9/25/2023 10:02 PM -%%Canvassize: 16383 -%%BoundingBox: 668 -724 1280 -478 -%%HiResBoundingBox: 668.8522925662 -723.82957172635 1279.61121764318 -478.292207216795 -%%DocumentProcessColors: Cyan Magenta Yellow -%AI5_FileFormat 14.0 -%AI12_BuildNumber: 80 -%AI3_ColorUsage: Color -%AI7_ImageSettings: 0 -%%RGBProcessColor: 0 0 0 ([Registration]) -%AI3_Cropmarks: 661.731755104687 -726.060889471573 1286.73175510469 -476.060889471573 -%AI3_TemplateBox: 298.5 -421.5 298.5 -421.5 -%AI3_TileBox: 565.526869621899 -886.698676336808 1382.96686252656 -315.378669012589 -%AI3_DocumentPreview: None -%AI5_ArtSize: 14400 14400 -%AI5_RulerUnits: 6 -%AI24_LargeCanvasScale: 1 -%AI9_ColorModel: 1 -%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 -%AI5_TargetResolution: 800 -%AI5_NumLayers: 1 -%AI17_Begin_Content_if_version_gt:24 4 -%AI10_OpenToVie: 656.148966433662 -393.435617140419 4.59 0 8865.48883571471 8369.3179700816 2950 1902 18 1 0 68 181 0 0 0 1 1 0 1 1 0 0 -%AI17_Alternate_Content -%AI9_OpenToView: 656.148966433662 -393.435617140419 4.59 2950 1902 18 1 0 68 181 0 0 0 1 1 0 1 1 0 0 -%AI17_End_Versioned_Content -%AI5_OpenViewLayers: 7 -%AI17_Begin_Content_if_version_gt:24 4 -%AI17_Alternate_Content -%AI17_End_Versioned_Content -%%PageOrigin:-8 -817 -%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 -%AI9_Flatten: 1 -%AI12_CMSettings: 00.MS -%%EndComments - -endstream endobj 11 0 obj <>stream -%AI24_ZStandard_Data(/XgY ,M6dN>L1{#SJ{I{ -{| $ q -qWXDb CAͦÁH@ :=+1Y -~aϊL^(.oÆ"OqQ -*R\ҳ{aLeƴg~9/#\$]bAiՇ" Eo 욆"@4I D6BFMaELQ{4G0EPµ kM$H6狃HWx,mzDYW$ͪ9er^fg7eDTE91~y %PhWL0 +iHp8Cw #qd8^4htՑ2S|!YZ{(_pb({vR ͑06D%a׳Uv㌎ ]u/bբ.sf'*2YN$sqfΛtgVV7Ѱ4sdkV*C - mgLVuK^Ҩ6KlJbC+C~,SoQ" -D$LDa8$ʮ!pa8բ0'0NpMqx QpB(n)/PwnGE)y9qELsBaGQQeR2v-ej -ozڵJ޲ 1QfCcE.:7b8pX\Qc8ʇp HLQJТ®wP<~i Oa7b "QGeH!Cғ3 zySnKp0D&@XNT  t5 0q(|^% ;CcZXVZ &1 b@xa%H @Y$"}f.C<*1i Áʮᰠ* D<g8o8gVL32m=D)NQ… 0f -ÍtqG}15JTp"H= B/EzV5d6f8粈hFCp2(A -f8(pȎ1^:) ["")#r1I0X2,U pH5ƊL$:hfu.Jx 6eHStٍu9:[q?`9$dBVKı3 ЎUs%df=Ϊ pـ#S3E%c H LGp"[ ;'8 R(ʆ)"q, @phb8B@(aEp EBqGaD"a8$dpH ^pHY-hA Ápw=+Z$DE) áLAXFoet! 'R-XBBA ZT5+G%J1n7:kHtEа# \up‰9n7 iP -p "3Iϼx2즚Jhf6{c,E+9YzyeH9 ɜeM'ӈ\q^ګV9YVKarew0U)?΂GV|^!CTh£vK2%!͔!o;8fP0  -$ Tݦ &QHE*P*Nq )F1Eb0D"@$#a$O8L`%*A JLb @$ @GaD 6ExxQDqCX#q 0>0qu1P #-*l8$2h0 qP$09qq4;+Nc bR`2A -"v;5B')yd"I.nqpHXGp?#U#C* B a8ppÁ - )AB?,4ʐsJ2e:_arxSK. 1ͩ)P8P@p0 {ASTU`߆VW-,˨(E-j쬆fvူ)La ~)nqSjzmY5zD]tp0<,"&*p&fi䑈L"9&Co8+da5H 8Xsta=8đ8! -aCDq8D@,D%,q&0 ' 'đ@$ Eb)Ja -S -TŠ+ š@( -b-ja[t\x8EbX,8~:F0 hQ\÷ᐮcEf$D[f8vD`Y58 4%$Wp(~`ᠨp0@p4fѰHu@(WV0J7F30(cH 8 07l8 4fXd1+x#q ~ +FN\.E*244l^ZVRN.p -L&ͶZ5S\wq<ޝ0vDe땅պ2 ! N'SґQbc|<98΍M 06`8`p"C -2 ,lA XPAB PH$`&D@6 : (@R | L "t@Ba < -$ -  h -"@p h -(@`RA"\BHH.XA"@0X8A -*\pDpHX`*\ (,H -\"@!h ,@b -<@`„ -T`T   -@ -DB *@ HPP! -"B -$a$,@L 2@ <0 $D ! H - `(H@B 8 aa & 0Á(3@<( *@`QG.aI%($A @L $D ,dA$ "\,<@_A -D&Rs"AL>L(fJ&Crts+v3;`X* aAx0tʂ -<DxPAB $D`x@p@:` xPB &*ULvTDXE}X%H!^U&AP`88d\DĈĂL82 L8(p0`8$@xHCjXPBB„DP&d0A 4P`8TRAp@\@ . Px"H - xP`8L"L &T@h„ -( (p S A -HI8J*[u,uJ=zirI&"tK{:txhЦKڑV!l&<+dNjw%U K+Qz?*3r9Tx̆lISᝯBG$fg}>5a[DşL,bm,0TUƥ:Ye5oW4yY^ʓ]n{eNȌwL0f[dwnSn'JO㕺3tw'sOi#ߋ (-7!. &و|SQ!2#{s.}7"sߎ|~t·ԎT4gs4Wi툕fr"rhM3ʣzac\ʤOr5+TGޙ2&b)=O1V~۹Pof$&*)sj8rTsEEnXJD4heUBO YOŬrU1^]*AjVSBd]HX'g#+twUWXN[*]Xb9CJh室.IMDnKաJOBg~z]n[>,NzZnRd Ut\1kV>V_=UJz"Eܫm$x5̻24-ʧIttW ^`VX>TUޕb1*1,!bHv%Yߓ)9I_/YR\%H5gs4تSٕ.IO魂%D4`I>-ݴ+XrJڂ%4',˻Y>.L*D%bHVU:%xyteBGnNMIǬ̯R*reF/ֲYlR2xr1CMN*U%wfwT!2G%u^)+I"VzܔPf2C䆊yZJfsxu|Sr8 \ zܓIX%K5h*bK9UN =3Xqj& }3Gy,t R\: "d 먤;gЖbѕ24tPK -eASeZ,oSqzWd{MU\S&$3%3O'Q+6 hUJh*q#VVw+nR#6eC\eS6]ksrHl -\u>*=*3ٗ%d;)ɼ,#Ғ.+#=< HMxUzĪPIٰ{Bw1щ^ƨˎXMdZ#DN}tSԕ]MG-XG4WBuBBf3Ւuve4&$c13KaK˔deK"[eMKS|&J`9mkdK f1RܫFNIx7R_fJvTjޑXXϾ2ң3}K呼ֲ-.b3OXY\e&"iύ+9"X7Y!:>3f=HMgʝ)wsKG6C ;"jS^^G9,Yi-Y+YI%2]S#GڍC7ڰ3~ĊurH$uUF$R7zY}|?4X㲑JUiTч^_c\> ض2]fK4&;LI$RBD´TG0o^)k,>"Lb!?ptIy#ٔi4t-IDlȷVli5G RO#*y%T83R4:YZ*N)Ϗmk5UfyR5bK~jRi2%r,J|gIv7+yN+FnJyʥ_GTZ"?"xӱ;SYڙ¢鎲h4!fX牖Ҳ~_5/.L](4-Y"ds#R֦ o)wDǼ9_0ͦ̍ ~,dDzXfăstD$tԫ!`Iw])Y):Ubͭi;Ey94e_EElRR#titSΈ z™RvVB_̈;k^Ȏa|i){Ht[zxWWC}<97֯Y=ySJ<#QVɁ)krݓ<+:w:Z@K(0 axA -<@B\x`F& b -$LP -(sSĻȊDM |Y"XBy%,g$Oҧnnc#&讆7ba >7$yXt7X?7﵉Zsy:ߝ,T V<3O<ݼB%g[#"9}nUXnJp*am}f-'kf ]55XYjp۞iR3` xj\8V)ڮy^W8XZC_*.ꃆFWz)VdTBBUYYh5ݫ.14d2Zi -{%IvMtiT^~'Kz:#eg8<sV/*֖fnyC)3V<v96bNK{&]U3LI)˞Д)_W*D-Չ,k政*Ty,I9FǪtY>#`K)6C%KM%sOk⫌|):bE -,xFV -KǞKH%aI+gzѝ8e9en='2=q+,W^iRE|]"Y*m\ OSXVt{nͷ7#;+uY# h5'+֮8eKgelĮX6ʘ\Ty^HkHU=7''w+W4r)6d%͟6vds]`"R'&ogugRiFQ3yCWN+ʵ9 5s֗Jp9n.1`+d"f24j^OIts_w6S+fOQyZn) )5|,,}#)fyj\8;ͼ0iR)Z7R?J*즡ۦgoa%fJ6i߳r"2aN~SD},!$C+rӆwXgߦZdJVyźŸI6YP&]gK. -MB"CDCTv3o3#C:$3B칈n-|FŒ#aB -*L\pq H@ -@*"֚B$'n}ڧy^=1:JJ/{ˡBs{j5=޸Ţi}6Ep;!oq>iB|jO<#ϕ"ıqPy*c}{t.~/[3S<#*9Y$Ek{/vQKY5KE|շS˥'-\Dd]x͛m^9;sXD/>c Lxt!yא_*JS|4DsK}ЉR5,Biz[BOߣ3kCω fɖ\FweahfZj_D|zɹ}U6*y2J8Y} -烦YfObԏ}`q;4|%x]UbJ S/E"m!窼J.3<|Ib*N|?+>ěK9T?ybf;iS ˝*m&Jl'^N⭬ dLJS*5w5y4; -We{Ouj26pbcyŰe\+-{XrҴ{XICrmrVR薼^u*<{2x5flf7**v_!2 'kNXw|fa^a^.f.<,9u8uaf.;Ygp[I2zl:Y;Hepެq}^]܎eu{n|mʖD*1Uiut镯JX232VN5i²F*6ʛ(e?$l-bmm5?[nln6ټlx;tto.#͍m>tmќnvݮW->Wi>U\}~6W3vDWUl^Gוc,5^Xu©=[wgeʬ^<_ؚ{jrΚٕ{sяyşɪLVR*/gw2tL5rP3n'O"HD'3i%=.?1/$EvLWMTt6=3 (C:ɎkRyu7fڙhLJ:itsx%}cgٕIe>e$X|` 1.}$1\+IKR$etGyru:+K -T' -L^YJf!0ωxv3dezR|ĖLE-mhffR-hB3b3f?xAJ>8F8vVIxjoJ4*sևcovH*i>S9G9h.&BIG&hW%:* ]C"7 ȩu5"y+l6d"O&*sgDu92Kh=|YXdj*j|PZbU>:W.7)ggehlVӕ]eAKaf ?d.U v-cOz,&\~eItѨuYzaV=~Xcofi؜zhnm+#TY&~98IJ R&k,尜Fw,ˇvY/d25u峕vcCY5{]}ձ[uWo Nm*t/5j:k]Q +_q>s$ јL. ӪdIVslUm*皕}3O'Χʼdeu# >d I(GqT$hPHY-'{t2 pv -'gvÇf51}QPe=r7O|)b=X@\{M8iFɿYa nd8Ķ~;!z7+ -7)=}5Kܰ㊡KbA8U8*͚W]MJ geE,L[g=)DɍB)sYy?e~2J -ȉ@FP8M^d$,?ף|ĥ΂NъQ;XѐY""Lr=Lep@^羥^gr 4Op⥖[#FSch7P!MR4駏ߚ?j|n9K7&?; 4 --vC؛8 ݧ78w,:bJ8 -)R)SEfчyð[ԛEjdD7XfܜWVofqij=Cfg]&YY&~|*a'@ -DKgkx]Yyr*:&^kBь5!Os:UYZR!o<]8kP]VwGw -B8u5F2>v5*TfIY-hs+K$#%Q!Y/gmGK^8L CjpqTYy9,qָFęԞH,A3z D ڪX48B sE8nSIpE_\ A|;Β?N<,kTȎhJ鼌}Y,P,/5cj>'ިyrhƱ^n gb딤Ym88kZz' ^8+^X|1 xLzI8R"'Ί5D7LXXI<ϋDp\,QwB 1&ٌeI7i<~Qg -/>5!SPKZ>TZɑ {t5Keq+i -]!f(/ J`(J0ҡ,"['V`"Lon[d@ڽ^@Ad$gm&zɣX-RY$ZC3BjV4裲3Cc78[S,YzzIغ.@ӟ2 -zJn,Bj:XUah˱UiLg%{{=63 qVybmVv:-q%Dʗoh8,HҫdB.YT𣏕Ec+=drQ(|qi`A!EyĶ&k.=ߒ 'S -||G€$u *;5 !γ>{Qm ֛ -1Zv1 vod4aj]_ L{A)M",8"c2詢Yd9/zo\;{;9VtB|(VFA$u%9ɄsCXȣ5(=5~Īo?|Lk |Ha%+HӲ^*Dt={>N!.o(MQkPYZ#Xei$Wm̥Q'?^s=ir;[F -GNw.p ~rW \`!˓/+QgJ;]s|Hss*w˦RnDx.ck- *FT,x%|= wüA>`mskdS- -ǨLUڀO:/2~Yrrnc/1IadjO -]=hm5W)*٨h29%F-nxQF3ѥI~rlxհNd7У\r(@,(t9Y[!{I -hC.Ym0&)y[ڤ \H90f"]Z# =]T/߸Oo̽&fLjXC+ìF#0E~x⚤1?>$o|uMJ"p1& -cv8Vv9%p\P Ed_`Y0o*đm&? "~҇qJdLd|B_[>ieU{Бu!}AC A-#/VH(A 喣Ɗh Ycűp6U9Tn#v,ܤ4(QJO V&NJ uO 2d$l(^şU2o槲>щ)F:6ePVxo8AN -5x\Sn W&% w~;+ ~@LW4- kDc K~*vѾ׍U& 'w߉b8EjŶض,WF6_Cfxz8WKC=Qv=pP)AA%5J-ܐ:a$>bEF-@-pcâ[Nc Sw&t=Wt1VUgs\5`wlIQz~ YB-S:k AuA4 - jeM ==cE1l.#.YXYF@B)$yy4 TdD%Ss,A@κSMd0U ) ˳Kw6@Qp -͂1ȺdWd:!! iSݛ#m$4ʔԈЈ**B]բ(23+|eQw ׀~q94gҏ |@@>vsQ%ais?FwCSJ-q( X$``6@$jA2p7ˆwzlYK/{=*i`  /YD%Rs)8d.R艞(ߔu;$}Q)ukaT~_tIe׈0X[*⁒i=趩c\2+)g)gqy{EDQ5S^\/v$H{ctDɸ5OMiO8kPZ~P+EȯO @:fa ӈzc]`!- arQOQDۑn^ɵT8Kʳ_qc5)8#yUzaϸ۠4T˜:|J̋yT62X|#3Đ/~QM(KuOOWRbi< -]0iF[ h`ܬLvK -@' -:3uI /F;Lx'xs8?ʻws Q/4>,%lg>r[]@Cge VzQY- ͒܏Ǯ)/VGBD3 / FZ& ef -X̨&VKJboӝpArW-[q\(XF殤B|^NSzg]bKcK f_sxK,V\b,9lDߨk=q_II.v- 2^>S ;A( ;ҭBS2gixNٜT/x̢R&}Qb) #.K, | h撜`R50"NU窣kgVd'`=)Y5-ڥ1wڤ_f>8^zx7dEWuNVcƪ8\A-:gWl'ck)PքK oRCڀlnPlFξ\G 6(wB'㝘5(&45Ze% KRM;7#,ZaɃ, vE7Z3A,S0W8>ˤ#xc^E!Px -6dyl4 -?RHB5oqSXe--Dr]=ҭ({m"F𩲌/V'#9DoH&t$#VɪEq>y#FJ<0hPT3+HK7iO>*7K I#+3Bn?XΎˠ@iWX$h()%f%043JKuee)bM1՛xL!O6tKU4e -[Gu6IudM `\hv8,8sRUXafl&Hw:sW,&T MryoEYx!k}![3)fYWM(€hbEHO$u7bϳ݃l/?-–^'y|hH}VS {"`d֢؁A4(c"e.Az8eHL74} O~5pܫSgy=,"eCyF3(4 :%޴`7W֤=v#Pjf#1D\Iq ޼=3fqSYeƣZC9awue[uD[,WSp7-.Lw:O0g혦VdJ`;E&P:1:0 䦠;qn#z9ի ~@xB%X$`^?|n!@>+ -hnp~HZ K.KRp6щ -ʒ7.Vj Q\4oPΉ~$}vxJQ*+;ۡJ7G|605AaGpUk,wfӾ|Jk|G(pcP-Nj@b7`ʼnoprbZؔ3)m,,wVaK1OI:\4[k(D&1FH긕oi`q|>Ɋ>x\d|~˛؈З{ -9l0NI:Iܿk I-ͳ"_\a +/:~al;78t -⮒P{짤YAj-jx,z:k?^: EPO ~˛ ~rj>bFҩ͌( y ` k_O;9~VXJ^ce0r;lZXo/HI<'$ƲDyfVP KߏdY*K{YZ^0_"k5r -9,)-0.9TOO)D5pv JV.U\Hd*.@Fo< t7nIA0؉`~'l+iv? -Zk~ǜ:y:1@ H& -[ zy l&fu'*pi{9 4]_P$3"HIQ\5CBLhʢۊrd;J!/iFs6ڷzQ<W-}{8U(CIT-;>J&]P`!6=w-> zF`.^𡇁v(uږz O\ǮM}?Rg=5iVif[`-T5edc\5X6QCSH7QD,8ar NTSb߯Toڞ<ھbe[#2]qLT Bp62RG?4 1%#,qFRV2TB}moï@!s6%'t|Jv9Mb#WyivP"0~gF2&gzJ/*0BSȢbVf©s/nE@:A+OP_bfۿJȹO*~ClABzGqΧh:J%9eBe$S -lΓ\5-u7$ˏCzq]bEHixD]T1mINԇ@n4b{őv&cñRwC -3U\8}P[ZnƤkh+|;Wөi5'4VSN5A@uw~H.T>cB91DWF'kNw]VDrI(Z.ú$\ʑ(֠9ѓЅĎe< 'L+]:VAX|c  1S>ճ)(0+5t>x./NQP"2 Pj-!b%=2T {h Ro9.|rfCXJ/lo`:%m ޟCBۚ "FhQXk3WF ͢UqLlg'PMq818U/,AVaش}0fX)֣r>0I(z_\@e_.4)0KbDaq0sgp]mD>/NGXQ%(Dw_Rd[ʪH3\n{{uSc~}효LAJԟ?%շsǪ#38@$9rIbk U!$t0G ;j?gF\):+Uè;{R#Bcz O\RE(h@.p ΞA -bW&V`r^DnnxwFʩ0@$fDΫ;QuKiߚ& 1-0p nNlX \bNe 28Px?ڢ+,8@2ǵx9qR"s{vΫIzFĢa\gv' C:+e3o+8#X؁GO`AiO8p\vK t@ -W?JrL΍:-Yx^QrnY6gHn'.*#Cir(gOgşHnuJf 踷d+x;9o,3lv|N` r|+M0бq6@xP駺\Np?[f.΂(Gdz,Ɣ9d!i熁dV\(xp n\ -;1t*rf-87zׇ -O,<$)տŀ:!=22>K/RTGs`/UYwWǎA2?lRcCT,'NYB7&>Dcy 雋983oNQ3w&1ǻDNNAjXTaj'(_W2|k]c0r;yjaQZ̘\<2>pTC-X'\7p`Z<'4-ac$]%cVv(SNELx?Pz̴VYVJ3eXĢ .h -v˸!5Mꝯ5q*N<+c譄y%;Ϛ>d/T3L]>TbI,ٍ`y -m g\n#: B*onz.TI%iP YajQshO''bڤFqFja+%zdŋ >P# #y`L@ww ],?ҡR9(jp2i3dzUZRRQ$UW%| .Hg *YZ7$,g -ZRV lUI +k[+/ĄۢXQ@o$gV+m9jFiҳr騫7尪^=NJ%рj*Eݦ dV'gpp$h[cqiz0ge`u#wxV\eEjhBzϓjfP}ڝrtD7G\h0QW]3BG)ʼ]â7(V|(.KV_yR/lԞEA20D_b4y[9TR|YY㘆tp -d'j4 R~ϨmPpG['Ose'Y58ߓA+U8SAV=R |?NAov -ZDG%7΂D Fp}%l -Bh)UEp9I?Jḿ' R"2"lR;Yx_ն&LOs üu&׾!ʤ|" "tOKČ hs~]}V3Ux2v&&X5-s$Zh:UBSOW$J# VIP%2>$7^/NjNE;o9r.F/\$Pׅ27`KE=r7/@j9eAdPǖ;&c^"ѻp}=IJ?m͠qH%ͦI@>tVtl)޹ z\F{4k -C-j51eZ*wu2J{$p&{M4 Mh0w EN>?0*?{mL0!0_}X\J+y1Le!l8."P@" 9 J:.ai;r`sh+)ʄ n,Tr@K%M' Ә jڭ|CO;d]"1>yJP -4t@ίyFSܔhy$}E=HY_PZr$\0% -M}-AK)# -^o&_7 tBo;$R'ӎh7 -Z+VIWEW t颿W6Q3|@,>L3qi$+BCr´qpqm~yQ2=?}bC!4޻[y۞F=spZ;nF|i"*2okmQQvAzƮjezz Wbd 1)i/gzNV![+I\K6JUL}Z' -^ke_=Pd(냑3.uW{U_[(l~@8!O`Ɂ:cU¸,EYR5`@d߫΅fZ 9ۅ~/3WT L8|ə1]R&nl9{+ʶWuj%/Rs ҉"M7]#3) [c?xFn{|7_Wr-@b$K asܧG%΍0b2I5ɕMw^ftc٫LOoEg\ًB+|+k@ hU7 D//%"Gn|R旆nXA -ŋADu0_E~W̕TwHZ)c%D cl1qDlPXnbP|`C!&hh@eb+1%6ɱhR%u d5Y蟥SL^F_M71{X!fwxpRW,U>$0r߲RlyysbƊumV{K~'l-s)[bII$ߝRRLI{ԙ+XK YNy%A]Os_$\ҳ WL_]>86fWm8F'M!fE ean/Spoe8TByOaQ_o)85-շRپd:jtqXԦ_TTSj+Rҋ;oێb GIɎ}~<SH-1JXVWq2#X' d0z=" eDNWb #aw4wm/U H_C6 3GA#;B꟡ "kz -ߟE*AjCDEC;2߁efBqyMnIl3_o2y-o;N#w[ۋD!\+jիg7#,͑X2~UtyT ]ӹNjR2xr$iIybˍܝ#Gn$AU~QưNz6υfwz9 ^],!J)Ӏn/Z ߫ɷBryӇH ~ /J˺p?JB."E.2Y.!o $xXAʑ+Ij?W|ȼ| -)>ۢ\ ]5d{ΐ|[^bq -Ѫn3|x)KN2Ny]Nޕ{)jB3e}cFZX*-C.wz/ ў˟o -.ոY 2$PmҥHh: apN`>g; ఺n ݂usӸG{3H#VK-ږPvzR~Ş6;jv4qwhQ<=/|з^aȰ͋"Srhts zB%˃^8P>zV̒k@I2nŲMh5|M˧R>kR{,{<9TɌ2H|ipTG{QhlɯdpҦu``nzHy1h~>׀pqd غc -oMIkIZN6E|Mtgt3(("`bɏt)fLL6Ud @bܤ:9}7ΉJ"[h8Em.%"=b yL'Q*|imG;‹kRk&gdeExxpZl/Yc j/0,$d6'i]'&L4:;Җ{-ppnKПgsEX~MWF'Bp>Lk-̃ZO)vxQc}& E}"'ho,"J}EQ8hF >9Q\mj_p/I/dyt4jpbxX't=Lz݈L{-3pD)r WԣY„֐AO69"荸+|vÞ ylRy{p?/+ƥ o9DYOP{B3\g\g !e{X  $m `x{,XtC wY"_(@QoorGJ':␕9f, :d_~Ugz̦ ~P0}aY؄#5"!]<I BH|1CN͚(\9 \7sU" wtr.*A`b!q LLWwf -/>' {\=@'bUlb(f?c*Q !Dz9q?$|[~~rxIЏp/L9c-v̉AY[8q7PG?[$!]+AFAKߔzWIfJ#G '+@ WDvktFܸϑG?Z{w:Gu)^C-qI7|67z>[g~m܇מAb0qS>^XMd4/NQɯ,"$]L oK9 .ͯ+R/h`|@pq^#YB`lbYi6!V=xt0[Oǒ - `HpiFHz52m=+Y9~8]p#čȍ"ltIʍ ^5Jc-]^jT5=C6 Hl<a+3`W/@vu4Y_ 6D㈹S"dh{Y2l{D@6!nYl޴8Qb}HcYw:D꣝e{.h›xȹ3 ċ\ '^ȩ~lC νeZY.CK]lG mf(}Z2 1A{& `6@pڢ $WZ 0sJ }y蕹̎04=OQ qtGSNw~V$ ` N4- Ҹ qa5JNpxI7Za$ 0z|腇x(sPnz:!&KlRO w2n!ъJqh\rcx6olՒUjY9YRۈ4$L;skPAeԩXs[N֎%H^V'E"+at2r}bcbz]~3zc [(("r-@coLjf鳂)& 3NY2p}IUI⿡^P^՘6s!6|`VSc~  DA! ٝ|=r S!Lj(2b$Tz6SEQy,&Շyh]ƳCF ?yYU* -ߗ:XDDk+5pלs7ϑ ETx@KEچ?;Y4gl)}ϸH~~?ً²Eܘ=X̎gOw$%g ,A\TpY3hVJmCJn1q (\6€[O9~ߚ?;rO}ĿlgX6wlToydp>2.$hRZdԚn0\An\M:=kzBU,Ȇr[94o$b^¸n3./Vtu[xȎfZ:˘p/L~<4kDG@69XIDQ(Dhe -I"z.q"uazn/QA" gACI:Qn%b.@ޕDE1gy0@QEQ-L?>q| Q>NLmQEt b)^@~;YzhY,&8Yެ`C^< I6J(А>ZY,5:M G>9HB3^ MWG]gLJ~Qs (jiGնQNN:.Ue~=b2V-Ly 4aCTLHoh~k4Er$`֟^/kijmo Ei˞T&1ry;޿_V zߌr Re -<*\} vzA`ր+ ;_0qR5$KV,6#-fk co zK$V{1[P^('sh!p3I]sMZ94Pʘ$#)Dg>\;!\+-]A~j0c Q Krz7a"‚2Y^bQ&լ·RC;4cȊ*'97畜ʴČC#ġO-q |%Ϸ.=ҼF,o QiVs_\sL8Mr 8WVQ_?d oPTG.A9Ĩ#ɭVDEo2X/;!E%/ۇ)ˇzV*&xi֤ڴ֐]Ɔ*γle( )MBŮ@q=OzB1(ړAQ765 /~Bf)ŻbYPܐ#ݛ#ߝS\<"E﫽W"i`)k"V<}R9MvT 3K5~~8,ȪYE0;is &]ې: d֬8#tUt6uÆYktgPUcn. B^$W 䏹USri[ŧHkT2eZgDx/##1 xh[ss3O[h )9b $fx%nl}HgADg확W I32:,K73jVkU4= RhA4k^4c(})5U<9_6K ̩ iU&!6Ҁg:Sfa[#Rè!!OЭ;}H{GeH 0'W2h7􈈐_BK'nG6 Ǖd*c`4+#}lj E%t} F -}-HOS}aNLFnZnޞ;2b;zKLsaA- UBnۦp;S^ڐy;@:TMLlE]VK|1Z[ܷGY^'Ӵ?4zMu`9ޡvsXreJC_%xM R CտP0+hB'Vf>Ouai/` Z pVHG.R0`[Z,UPbp{(o$|ɪUUl[ `]X$8SZ-k 7D[H-_x<^69&9Si*' ZlMIZ~q4HݎV?:^R!V4L=ɩduZFgnWT^Bއ 'W";Ye!/\(9fc{93W8|^#&Ew)(%$;&t[ew29\AMx-t3rbƦ؆{7<=lGX/S 5ÆPJvUKI=;lЙ+{DQ ~7_C̾>u{wWj=:dbh۾k!f9k,nˆ-QQG~ %= tz(A[$2KEag?E a;Ff }7 J_rDff,F@`k#Dc(i,Ɗ[ 6WwCYy28نg(u,>(5rоL CU5[4 3m[q;EGz{Na~k SÈb=2>+TPq`)F=i)8&`2i։?qF=TVM\v11𭪰7zlM0h&p?J@ŒaWрJ Ine_.P[? 146xo4kԸ!nG\{`٦WhrBNpF6>Gw$yP9ч*=W[;t>Տ9ޱBrm2?U(:I۸^Qɫ -֡\Jm1"af?N4S6PJnP~ **P@'61k*Әd B2u}}uȨ8zMہ~!95P[QTfS'U z\l6 XgQ%c4 eA% tՅJh7aK9a@{GZӣ|pʕO^?sukC1~Ilpwv^]m޸?pQ "$!F>;-JWH0zo劣Z~Bl_7gq?oaRӴ9*Pϊ] XT_\2iI7$)qOI~M5`,?$A^^cZB1]ǰ?($,Wp*m=-o>^n#O\lQM IksЍVB1X4 W@=LI̸^0Uh-:鑝#HތD#zFs`W&R53( *#Q{ COEEiz %&]֯qB"XSҨ@EL՜,it[I~(rpdx_ԥg\?ZϪS{o]Sr Sߓٔ_a:aK3kq k\:c} @,y[^nt4):Pk ;dHa&ף-B(KI_&vSܥƵQ)j].#2n_,{e[jsi@rON&r-WJҕ4 n[:uc4_"j|Eu hJ<ЇY*o<WQ,*~57Ґ(<iLiFe]hE"70WKiGhs/L igեTR L1*3H恰rthC-(Ϻ\dvVde_]Ik&5:#TL[jIk> (2^@o3lٵ`zVpsflDž߇xysNrgN<'EADT(3zUCitJb+kO?;7m0?/=z]&VhW% nh$IzJ$)%y zaSINqd]RLF"/JǬJyFgu?Z{gmn(-D-e>~)i:/u%ZKiiz!7ሦEX'i]M)RB)yE=tIjvw]yG[NnΛS(F?Ķh% ʇĠM$< ѳ*!̈'ď"$(+iP,rD>܋_4.JO0=]LЫ;j/zO¾?<8喾!Sg&)K HjR5]ZKyHLt$(7+ #AetpUH0J k yfY=R\{xƒMHRW8 G#%{>M~!̩DCa7!1RiOQ<Ky~L 5lB"qTED"BcG>Hk! -$Yn&s=HOvWtx{ׇqX.qy:}3|EȞd%=%W͕+4D#G;x~8?x8ͨqLc>T3_EGHi!jܾȊk}G84}ݗp19}u} 'o[w(~E?D)R'QXKqDA}nP!ihBFPq$HӜ/bDeM#֗`?ssxaI_cP]zF%׽PdfXM(4+qSfFk>?ewab=>Ri)❚虊sZs❖#VS9QRu>Tg [ƧJ i|gZUW]tx"Y%jH\!wŢlR}HԕIe]K -wq]uqG&.eh*u#; I1zHƩ~r*!-㾽aM~͔g<_$ҋpRGRWI$Jc-Sf5Š 2*N6'9p,΋[Jh<B CBC vբ&kqP]K+*nܘ.>[-MtqG21ѹmGlhbl'lfj"H #3>+˖`QAR_j rLɻ؍iui`LtT0 IQQK(d"둊!_Lmh$i ˣhS4hnC'·&Bx!ͳtfP'|EJ!sPj6!(d&tffCaffs!:*>9 (F[h3њCFIJ5>b _b3|Ƭ9_EQ4bBmmVYptɐ*xDn:SF_E7\ f^͹yݻ^y]_승e%H:AA5AAl22bC/ˬGW])H%[H\JNDg䗷)DlDf8HKF:qitCyy;ԸFJHfju_8I^LMC$|UB$8}e$C;v"GSB4GSh v4SJ^> JPdkD025C\Z!:hKVء5>y!:FHJgAj*WrJS6,3S$+kf$4`>g4-FLQ?3!qR65(~XGQFDhx=^Hc}#d>g>voS7ӎƙbBvߥ6pw xX\)P8P(O)pJaeH4IU*ipѩiꤤ"n(2ǬZnxY!٫%LF%# -n䢬˳gw*s{zD(}*CS."7+#"@6d a([lr`q@%J@*y bzuzJ%"'(b%gRRGn. -|WvaLUxdHV5ĢM|T*g&iHѩp Uc$60b:bjr%N1|DTz4h153ェuWV:Yůؐ:bOeD;բXk,'?EfHQ]*E%`Xl yRòVv`YָH+-=Zzhuf_Zg_>H!]mkt!ϴKҴIJΐl\\)LuCgfR~џҍ1_>'iϡ\ƅT_en[_Ή_SAvUBZHIvU$N .rh~)͏2h~b vQG1j( 4RR"6="5_>ΉEe Kdz K̄ӇPzbl>EEQ=KFGGV(Yt_Z|H5C9GY!BV4A|dkؑ}K8)*;0$ʢc -DNxBZIkQPİ"ƠUp9umIZI'CNjɘIg$PPѝ7d -fdDG)4l>Y5i-N? u#e"b,٦igabz}#G1t٧ۃ6/ʦ!wlK$z|qc7c֠Wo~arr1*|"g*}b39HPjU_!X$a]6J+}'3;+Lg+R}~_'٢bc?ZN,ф%ф= M-hh%WJQ'f[žUUE/#2u!ϞL/mtZDǔiZ8><+]&QE'F$tO!G6g<^BE+2)u踈uV!"j:=>>Vz~B֒ZDVlm"EҋrԸR9*Ʀd7rw6U1'yҢ]62:PJrPPBIIIHJ< r&l⽔^jUE$Wu411Ux8B.p0BJL&2tՙd68i]F#B'`P*r|ҵD~m]%W2"aWݪd^fdCxbeIl4ot4УIcu5>uJ]$1ԑ_H'> R67dF7 E^Q }MBҏR(GX -)X++C')x~B&tdMFФ9M!okS3l!)j}/u=oVj"92E.FQ+`gd43##epм#'WhsьUW.C&#D62as\dF<'jr3#4:8$ʥ]}xZќK͉`EzaG# =XУqf/RnpoWO&HEb\\ o^._:iR* c,I1e"a3QBGk#D+)ӂi1B%HtIW>Ō,ukH:GygF!sVG7G|&9ňv)e/#PQBIΚMOZ^|QڼZQ/f*9 U n.1&hfpU¹5p.ͭC7n&ֳfV3ڌ^n^&֩P#F<cďyH nGiIZFEND2Th*e2 eI+ȡ)&HB CK\^y8Da%1U)mP\{[%%CV9 -Y$ vBs[JCT6SHP=kD,)G#+4-05=("p(ȫ[Q}]=h -C(VZ9IHIꭹED3ݘnz@EEڄEؑ5$G5Zr^q&SHNY~}ڡ'Sq~B:ca +.s&%S[HLG8WE>9" -Qz˒U%hw*Oab$GhE,4%r=9(O2ǝ,VZi+CX#o/mE$5V +fdƋ7ZQ;j5#U\KA +dߦj(%ũ"V4#4G#՗!,FbB:7XiDY()>JtIf.;Sr -m$P|1 O4$Wp(3VMd -R?2eW?6CIe/[jЋL1b"CXo@iGYAEArΉhA VjfgK*9#슯, z8"1C: bʞȶ3DlQYѷĝԯ#LCbÝ^S&r)dN̸h"l5r)H"bvexe1|qt8Y-'UxAD"Qgg+ ,}+^]^bZjǣsI-[AY Iz br|厕 bQ!Via' qy([Qۣ -CoU=X1E8x1GkݪRmWU"VL˒ƙ4r%IOT)Qb뒤-⍡lrQh9JLFVCX(nK$]"T.Gcӊ8Y!xp"iL̼Rٙ]>OJMŦ2 -<nHΩpgʹ'Nk])?HsEmzтKG2iJKO*Vqœ^"ntAESt/ iZ$nfit8aivG"X]Eȉ~rl_$E-٥M6mutM7ӕ[GvבQkE)\NJFP9B-$HX ]&10Eu3,+))^-!+[èYNC7t\B n"ʡpPeIC_paƁ\Ύl^}拠EE/hNJ`e2Na~DD8a5A2F""(cϾ \-""aw^̅qab r  -CW !_p4,dS't/$").ك` -A -7>rp`w0)O#<"{  TY@Cq`A c@Ș&JL,Q?2#([WR'hAN ?3 -P7#ihUI_H)Jq $̴_0ZGbكS.AJd :z f<|,{AxSǃ5R/cvp<`W - ~A2 ,ځ2|`ax0fP㌗Du!"x`œ*M85`B} `po\|6sP r'r 2\r 9808A;ā0P-`pg "p`oM`0u#7ع66hڀmZ0ld408l _Av A5hf2As[ ˀAsՒ@>P a>WԛJj6s))x)ąbC-ΰ KoX;V3Y13(~3h\ `9(K  'z ^q LS*sXȋ RM 6`p% v/@VȾ78^8d(0ƿ s/uP) T) nl0GWX!&;@ -[=h(yș8 y.8 AD̙v\ `8XRp\`q)pD[`XYb0Ho0H` ƝQ=w ģCT:ETlLL%Ax: [,o { 6vنߐ@0PIVɌ+_a&j݂gĻ(sFu -H^m{vaP9?kݞ1n [Z وg@8a{ζp$`pWT=E!'J5qHt=)T/vR! %2ٲp({H} BIFo.BZ}ܚpA~z1 ڂiΞ· ,ǘw& ?j0;!wSQNPkctWxqWM0o]zH5H!^B $RC>aT&&. S2NLKpBeKK -]\ 6|+`-JI6rAixV: =p1[*vH Qgܟ'$ȥؒ@w%a|I  r GThx#ie= 𔦃{p:ܣ?+m}Gznk7oLZjD2F~U>gamE[#HGZgTMJϹh5@v.`Ġ6@D\q4S|sob_%X/Q[F:D*32)}Ak !o!${v= bƛMR6^ EYh ޣPK9 u攔i'gBpyJ'J!`X2 p<wa/rd h.@cy 5@V@AЂw -AH -xe=Gl)7:I`@g?_C&,^k.KVנ~ W˕g\WH'́ x&(EF-ҤĪ4Ýi%z%A=0U¡ XZz 2B4&yu%x,-8gƇ"Mž^~z`hЄs6 -lI 5sR߬ I÷:`/ YAyu@-+8.<&p9`}L52.yHSzbqUIt^tqGqἄۺ++}_7iXш˄(?fFj@81WZa{\4SpvN& -ȉz=I`BdG8PaSG}wT^ {RWNiӵ}R?Ԙp_7ptYEsnj8@R zBY JP>$(%w -&q3Z^v_~䱁7%%J> :@3[vHUeRai`Og)0m5m5@3}5ЬDPHf H;WH 2X͸h .ȇ O6l;% D>$g&*rO~Qwp*20@3S%׀ThO'!)'|I.@у1),'~b@|q5Op_dPLw%i[ Bao&?js;3 -*h  y n43wJ XN.PN-h[i@gi4[xZȡ=4ꬁ'+R{0U@MR. bXKy&B jL&(x^vh%>\ 5@4X})6\prMݟA˧2$^2xp6 iiPfv+q -i^; ]wh@T> @ 7>8')@:DMbz8\WӀ#hrr:= D r'.yyd̀$(mlf #s(S# bf4F 5 |a: p=41,*ւm"VV. qem V<2!mTJ^0OxV,zߺ.U60fƀy-Z)'1 -LtmOH:@  `@/Gy#Wn0QY0@ WW?%ḵG4/m!$bEEZDDg@s BEٻh'Gg:LMb=1[+ICɩ|q+`)4 zqHK -g w0/Ԡ}W9CT'8FDˡ5s74"+`{P4_5 -85xB -zwv?@]n -%DzQgw&wW[ -X_uPoU - -ʌַb̓AU@)5U\YU@rTc-O`4K0D y$ -vT -mU0Fp`2 -(j6[OFa -:MTP_U 4'jZo= -6t3HO MDRZ4_"+Ȏ pA}*`aDܸT2YQ<~[\Lr"k* 2ǯ] -?FfduU"S  Z -h6ZBm^Ӣ?JWw5 [S,,8Y -8e*U%^ -Zb {ץVΥU9r*1ʠ -Z;<*Z*iu0SJEWvAs -J"hqN9F8kssA.) -H|  -oجI< p&dޥ #P H>ׄFJxJdV ПJ>+yeIv4}S.[\^OcaBg P=@|ݶ$foV~#(e<a3$hZ µԭ'yhoZs |@c(R'`B )lQ_RuOюq&G+Ym%@EH.kdfS -"=='(HwWB:6#N?H`p Ҷ6a7Ġz]PR3,~]tֱ쏬~^N. ^^i4_:཭[~y9Y<=ӄ{]&[a $}VAGᲥ.6r*>?s1u9,oNMTV-xVP_iVCXSOڝW&m10ONG,t2됋N3=בTVھ3^Q @Z;{Bå',))A OT?N=uE_\wy//mZT$L<ǫN̙ `j1ϤaO@r;lv޸ݡqLق̩\}qhv/ Tt74:mYIowar(LX? ˍG_I(:#/V8ٍ¨̪M:;4W k|#g)6)$$sI-2=G C:H4,}I$U+&˵p"TF\ͳI)1U]ArAك"4%kzQ4HC"'nzsAeٟ% njjvDݤƆUG4n /(מAQc1-^ "[_~k"oqAOaIO$glSW" 9-~`*0l$4=(ELH>$v[FFV{F`C&r "SSQB_ *e~EO\͎?J wyLCً&aOxiG -ZW"!WДA':ISk_W[k2iK t=CI?4?bJO\ִy+Vy2}P$'u&dQ]XJYow w'#f=3Y\zg@g:c*hVI ):'`"4,¨3ek;c嚝a=bL)g!zrb@~R> mBUNGvlf q{i6$R MS}bW 足o, -Iu2,V/=I~țzKM2Ζխ_CZlHwfA~Kt‹XS?^2V'S##{ |"\yWooi @|\i C Kh"#z>퀲AF8举s[!k9' h?4.ь_Q v1Qy"vlZ+nSWODXzHXXx |}i󕪞݂5͆mCWV aGMɟ4.BI ; FToCmh)6G5!+ɢ1|^D\*pÝe{»+ )l83l`ޱoe?Yɸ-}c]˶~nE¤cVy!;>5Q7N.//|Gc$o37, MA Yv(B/ - -{V<I@gbC(+g L[jX=B_(\W`a I #F`Q`{AJ'nΜQFgcoijMU*0\aB&3⏚4c%ߖ(J;WLɶ -l)!Lm!\ðTG' -¯ ?q~U_~]ʚl1rnaaPhЈH#'¼$ڑTDaWwɦ?zOԩ)ҊJ12Ѕ<>|sLk|}#U2qI{Yv߿=]<>+o DHtu%*mZvӗk0[͉#Ȁ"ݒ#s/9dJhM]\ (QH'J쟾Y󅣭HN8.z-@f%?]KFMi͍p9Dszd3~Cia ~#} kvuCb}gefh.K;ie^78UZ:c+a\ꓪB;7?<PW~\fư x,D 8( ۈⶢ_+yҿ@.7 -J-#SU@ 6P!6!$<D&F/4a}z[_9u&& aC䯃,Dj({CX&$X &3VW7?G(d'=\*鬚׮k0?KaP !8? i2aG›b6 [Rv/n8*j[XD 6ǒ0#-oK2 3UBM[ d1_s[.0Z+?TJƎ/7쨺qw-ߔ$폚'p9XȰ>ojb:_l@and|ZOݚb -zLLb ʖiN[RǧPY@iVOZV -o -(кi~N$faΌ0>`v(Sa+ƂrOm-ZǡIK|š ֪ -кlSCO1|j9|C5|PGNb@^L0F&,#D~UrhF*P1z[u8R4B+m]ho& -Rl6X~,jăXOSA8.~4~d~m^j-]eJ oC#}[ws.48i|Ag;T$3/TCd <%%S&ƴj__}QUR?tȼ7GQ`WQ~FG`ɇ!o$Cl=簷{gMev{W/{x{aet/Czգ_ ݇e 9C1[~nx qrn*&^H!~zi(6W| ~|F`YZ2wJ -y Wom!1|QgC\ۻ޻o-/a!uzTDqoxGa ]&η@fePX5\]*yGz>!ߺi>6BIsaע_s -!Q_1 7}wPd+)m4y{rVT7^ KX8f`+ ;c۾_ql{B -jM`5mhBnwWv\iS%d #pmEfd/3,#E;<>t„VibFmlpc^ -QmjmPH1kX\ݳ R( _/S!= -C?ыm^x!'Gq7}\xv߀܌mϳRq.V@ -PߗBھUΨRab։ݫ? ~eseR° -f?':]*-Wm"Rquҟ?mOp(Ъ% Qߣb3tYZ:9ufuj{@5}븥>z(o^q^Q3j{ޅ߳WfU7&Nk]ʁWC Zg/7Njc%Q_q<0T 㶸JA {[-)@8b"~BNvkzV'~+³++nd+:y*=[1lZN։`7eo;(fk($Ã6NJE%u^чѭ~SRfjNt<(h;qVSl=X{R 3ԫ8J A #\!ݗ-Ct&Z.FNJɤ<&x?!B;d[@CI/\:HM%lܕHk)U&Q頑 Kmhz(uq'ZYGBd+(OX:=/y0HEChgPDӖRSTH}Dd:ugrL8PzO2*qgaϛ9tߎERj7̑`0;f*g\e*M! K\ҎtWdǤ9(4b?4d3})WRyq;bHN[sO6/3CҎޠk K#Uje/pU[Vqƍa`_Cޝ5zޑ~w[ WZ+vW\-xB՗Wx?OZDjL@k[xmҕ&8-~لڟoeY3!i^ < 2BrgL#8ހc1\(M//A z{SNb4!`N~ \Ҷ@?dS=;Pg J$2;–ksIq' -&7PDm :pt&TKܔȠp W [S*NAExAML"כ Р\)  Ҵk*]!@tۄFe -צev +J7$4pt4rzSZ&2讪 U tU`ֹkщGQ t!F*s%.VqPA2ӎM2NbIiHq:#!9ঐsU9%4[L&7a0%JcAlƷ ߂m;:"i7%Rhk(2mQ~pk:54Frcڔ\ ]#úlqchН)cT]g.YuD_'. WxB%1Zً'#=jA(\84iA22Ulo2WM -hAh%ܗgEAh `qf;ɦ8E>*ڴf)%QJ-8 lQW=/g -6,* W:; w (k/nAC4 p'0@$}tUghxoU0/hȞ.EdS -$$pŦ.Ґ~V7@lCQd`GA(Cns3(Z'Ĺ®-H%pZb%}U:U V9j(N0L~$EٍcSd=5Tv *(MD -ɢ|O~φK -NcfeQMn/,9Oj|MNy#a4 ZPRʁZEz xF6f_sq 9h[ u6%8tg 9D @Y6-kCB@aĻ ¦R(B♇xo7ֆvihAWJ R{ޭ4MeŌ..hH] G>𗴋'anڠI>Dj:;gAuH3dAǣW$6FYDF A 5ậ5IқrR,D)|h{A3glJCC6m"6Ճ*SӉqOrV^< ]:s̃ʍyЯyM<&=6faҬQ-Ώ?Kp -X (nfdM( zxl0,Hƃd1nj{l?HW)@:')1@7mJen`vx=EH -1:(60uIٷ& ܴPnR?;[a᦬ kƔ<>}:AB}!jYedb5DVq-tkޔEdj@cޔV&BySIoXw.j>+g,V.2&<'^rA.:PrMJF(.U =+BgNkM,=>͛rQTEHG*BB+ !ڶ "<}EtN$>5]z2m7i*BE7K[9D;"tBcgTWax;W5Mcܲ2=BhhJ5.Ʌ!Gguu!*v -nj,*YM025D[+Z;_M !e)BdX NODv,j,Uju/5@"C@[ ! !e -@K&x+KMf0B3WN\#Xj,@$B^CAZ`!y&1܎8ZI ^Yz!hyݧRBʀw0"̥璉L@H@慐{+ohax7y5B|Z⺛܆] J>ca9B`#R;%$ClؔAEwSьq c }PkT:R1K'3˨jcrI|9 C+䣥 C讖/B򀄎Rt .KZ*KMlSy!b7AMPߞxP3%TFLNJR!2F`'BفH5ebvT֚`JERB8fhĆ7+#XXJzUDt K'!xM)w6L(<'D}[jNڃbBYbH2!s&D|dBu$hʐ2|A` +YgB#nX΄ J׼N,A}FBOY._~OI\ Jx7+z-JHTkjPTIХ8 ;w]Pta(cO.OO'w"`0Wbve=pwN^TE -Ņ M\EER.#($5Nq~nw}k;$12HD>C=$?]sϴ g$#Xߠb@|@aos` #FlӁ"s#k|9x.V82/BAMsGc2B%5"fc5y4wflE*<$di%d Oe/J"r;"<z*kc6 ";* ICu@-|ǰb1߷~Zҕ'cʨӹvIf($Qy\*d0IOPVGqڂAI"(e -@<7dR qfA*ٺ@~:/PQ S)MyH^au'l"u֔HWXpƫ$[=qռJ: -Wo]v+%Ifl J\H-eJOwJg$|gi -TU͖ml"+Jttd%1q3,prSZ -BRXč۷AB'PА -HBR/ -y%ǧ4=rnH)7H -X*Iې޲r俨T[`i.a|[,LYiIe~ RNT(t'Fzo7L)*iU}XI$*AmܵW*d;}I2 t 3ɤ!%L!;Æ)r6\_ -SX+HfSڭ ֦vi~cj]R -S02A$Dfʓ`d/(H3(!L;4Ȕ7 )!MFLQH~"|H da\s?X -PJλIk?L9{_^|jFTXTj0U͈UVlNe/7I14fV`*No(&D71^g1`r032oM~jSHS1cǻ=n~̴*c"_+fjL}\-㊩)|$EZVLl>^K=/(z6uwӘHaHmYs1UÉ'qd,I7,9c>:$1I RgfQZeLG適uHM;E|5)-p$E„*(-.KKe".epnڤp&)'oSaRlLbANTRGK[ -d;F@L`&Q~#gXy'sT{)b& SaU.dRzL4beϽTbdYBR_L2AbXo1aS-nH7pPŴͪFV ] ,Sf"-ػC8쯂)Hn)a1)nbYbJ Tҹ6eyp+n@b2&CwN;:0b Ku>S"LH;nǠv0=߀:թLRu*<\JZ<1paŤpl07~xNE^.8Op蹘 TƸئ`1u݄x gTr/}r1В-I1fɘ $2)&WW= c"_d0pk!gi}?>f*띩"2xTsClʗX4r5ML挧Q"g5cS2.̩@˲_Mue*#*&kL`@dT INQi󀲡+?u.׏(@ɐ{jdedr[n!Sa5(5~m2O!|]iKmħ4_֔hoO",][IPRs88,R6conRꓥ+ -dЬa._>k6)jj -endstream endobj 12 0 obj <>stream -U%Yz! "'9&+y

T5 NQ:ż*B=GqNQ{!Lғ;$S0JgAeb#?y"{T]1kcp xVf$ nWWm€ -~Rył$n`8&3'˨KviVrGpM@T_.Ww\2[},vI`9nh\oGao*14 HbZw ӎ:>5UMRǘ/ - '{aȲGb%#U/J%_YiRUrRďnge:tY9xh{kcֳưhJ2(5TTFOr -JMX^@gџ ̕ԦŠsL㟪vg -Aj@#IʁX6Sb)WPL\#䗥Zr\]y,PS3Ti?*5b,i,W/| HuS*ը*rgF;bVoW$K_>,4LU*l=gyWAD HU4_TTzj*fTً8/0E*dMc'kqRCe:N*n+.IuFJ=3~AD!T+Ld]L%qlfvAMq -Ojo`U~{.fWx@TDMB0 G $oVO -u[}*?AQjaauu@MJV -L#(SO^m'=שr:ޜWh-/P:Vt1 +[#[W 8rfAPJئJ~51~=)b+ f -=y`U b%GV@\0RU+=aztlVZb)ðGW\ MQ9ys%`#K!m%7Jt+g9:̽ZJq +4ܯ@ɼXu,k- ?uq$r-6u} Yu|%X%GTiNV21Vxe= ! 8Д*k]YçmHnRNpC_Ub!?/ZVEfS/BqҬf ^xf_TY/(\uu֭L6$ϕ.k uօ(բ`Y:FkRiSLWۆgQ? <2YJYe^1g,w-| @:SHZЭ -oS唇jY@5CӮ ®gV肠΢  (Y8Βf:|׶~^u_7d}H[tuu8` Mko#{}2³ -p-@8E'?xvgqut -ς78_ZĻ'C`ljc/dgOI3r=$U!x,lQ05/R-Y5bU1L !YMWKSd,#KHc|,y* g5yP_=WΪ -V ۃY!*`kY>:qwVQSEBu5Nb4fxo<+ߤ5<6E0`7ֱH'D)|!349(gI:x9Vޞ@ƝH[1|r\owO-̖urW.A^2r kޓ-c:L!N,(J,,E\MsC-PsgPT;P{C# -'tg90 -,?*wm6 rg)i #MG(rB@æ8{,QDT"-E};򀚺Il\z-;BFJ:f8dg9'.PLKP Yh:5U,tɺ|NgոYfͳ[/R= !.9XIgoMi.?Ng]A@:r,e)nSg]Y8f,4S Lud&nl]3I 2;I_u:@w|ꬤai qф,[ 䥗!'d;Pc04H͂aH!m@NsY-ԹiP@מ-"hjs`W3g%AK/.YeqnTYtyՂ05Bn خkZrBA5(ظsۼIe(e@.`E1TAwBe%+L7 ;ql7BeIÉa $6[@d<FW4qpDh8_mr@xq`غܗ5Y xq7+O94M -Y(@E1 -8 & .)Ϊ ޏ;n{.Yľ4L~&κY-o ;,j08/WyTXDY^%Kn -i'z0_$[t8ijbLY 4*B! ݂8\+"?OWYjTj~Pqc:%.N{7R.RM)KwLUdIC#J1*"Z*8v}WC ,0JK1gIt \ł -}g!<⬿qqV"UШ%2G} Y#*1^~o?;ꚇX pVm2*}2Op8k栊qU(+g. -01#Y|x_ho YXlS66jэ$Bfݽ&zf' ->ð=Ͳ֤URI{'j6XC@ 'r"y@kf5 e,)D-鋬YpM>+7R1H좶fŒLhmլ6 pe;'w:BgkLƪxYEaMɰB`5F5&#`%Ԭxj:>ѣ7ZPf!JfB65KP1Y s_j1uVb28Ue^+ -vfS$MKÒF,H=qp>ݯf4R>hkA6R/k4xzH9^,%@!QuiۢYW ۤYd3d>;JD2ODc5]0#Y[at4LwtY'H1s2+f'' =&]j2o4 b |iVd.bGg4댵}Md5 l焔90$w1cK5oY3JYRCMҨp^4_ZM|UzjgawTia3팊OJc,vUC\sIS.4_R<#YD1 {h>yVT5-YjVA9r!Ii"===YM!SmëfOjވ4T2M]SerUȋcRU˛f+_ŧYL4Kzc ~xi[ri3 -RfݱԋcfeYf  i1,D"Ĉf9 5Wn0B0sfك6^4[Y4I75K^p 5 2Ca9&LzDE Af5ȣ@y-i)'YTiG0apR,G@lft;a+7 tIcUzLSQ#U*F\-I´x⇧!*AYP4y4\n -$;FLOF0RMr73f9ԊnPıfe:/f!#M⍿,8D ?5+<5T>~eVfՁ X֊(PY@FQjF4n/5bc^ 27E8:]M^4⏿鬻i s3YN%kf1)j۠8-ÏJd/BD8m\CCndNmAF;@Li,<|ņM!|Yeh6 -4h_=fa5p$궑hDhٓ!c,Ma4-9Ufj2PQ4  Oڋf}d,: h<p;{4+estdf ZjzSDenGa:mb@ A[ .iȜRi ̼2r S}c8znYWl*32ONR ͂k G3+9@"2j<^:?Di2d?!YhV3Ft"`Bi#*YbG>F֢oG_ Vf9j2((g@l0fl^sS..t՟%5@ W؁ø0c'\iV!N iT.([ 4k'!f Sd|(ӒҪ` '*f2*a5W)%/!cfiSb5,r`kr`iiֽ_J)ҬRiVXb`)Y犈/lgGmUm1R9Y[óT}(9j"f_ .YrYt)C$u\X7`.Bf즱]15 xMAa"Zb秾_?9g@vYWb҂|5X훿YH2NPS #Izϊ?Y>(wɥ+Y,ܨuoo[{aX@5,;#C䩭Y+]Wǣk\h4֠#ʳ?fR dZ -wkVO߯S^r?a@ *B5ϘXlId5f9L i,AC=͂ @VQԧYy]2\,Sv|JܦY.4Ku| U\?50Yn<-D^ʣl],MYamAr^VŸg͂M戚/lfXjVͬ@C͊`h OqOXfoNmF=g N`604+jf7LaaC7͊iyVxuh)(1J \ȝO0ݑPL0ks@4~_{f}kVMno* :0 -QkVfnB A _SWd5(K ՈCjYEΫDG'pܚ%Z84X^kV5 t:֬zY qC) oRY3شUk\|SZa1fΞJ5+{0^|i=q֬wLi%k5 s Gf~Z*[u=٩{PJ {Y@Sm8}Z@pQ6pÕRHͪvU5PAլ*&+6keͲ/ri,>kDY؀`2-\ -?ci8լHeͪ%if n)B5 ŢSdlYr,+-jV"M5 fLlĞԴ X>p/hY}=(EmhrQ,@/JG˵ ;Y _bi5=Ⱦ=^L.g 3F0e>N7l;gгo s2cg/w)9T -vm8X]LC,0oeEH U3 {ϡ4a'd b\Ab#,L0$B5)G?s\1T:xsH=(<ў&ѧ -Qf/:%LA x6ND)*f"mph>R!Q {+z-eo -͝O{&@&BT$AGQcx -tL($ ~xuc443̘mb9#s]ۥ f>a|EiE\? C/ ѠE2bI%>BCs~ Aaa e0eX{,8_4{g|¾!A9C 5>N~6~>pCGVc{ *~»㷐"}6DCp#`8 !ЉC`pZ ]0 :v8{$)&# -u@0pRBo{Z!J EMfLD)qUBNlN  ȃzQ -H70};J2σM'fC -aGHf23gY)Hߣ6,Ub9C+AW6JPxMnW -+m!3BO\jyCG;#Q<>CC#X.]L#SEKj9ChV~2W`y Ä)j 1͢>~MgD7s(0( -UxJA2l䃶?KDYvf ?eoUF|Gs3 'n;c7._PrhiZϥy4B'-HA6qž,Ie*/[pף+ahgFr_׊x; Iz4dWm,4 (,eQwѣa{ܟơ*Gc%`h=084s,PhXi@Ndk<;IrE|Hew_:|9*~~Ě`IP,"Hx|4pov,:B ].ۥJ"=?? RyGC$:q3}=!wSCiT *ЁfyM! +4T {1 @񰇮d"r zQJe -hJi)FxSŊ "(GLar/H1gvËqRLFN SƓ Z*  h/8l"l AH=65fuLG8(x)E!'qQ4䥿6itH!u/SS^1# -%] /R:HC:G7 N1XJ! 8S+ {U t $ aXZA/nY\A& }<8vGH1VnVy a1bO \ĄAyCbqEҘhei~.(im:hPA<"GA@|Ý - 2H<=]h4f]gm*Q3ܛ]E;ܡ -|K.%yL-*;\'o -4&\  Q N+Q\H#\%{B6hRZp4$XMR^}qtD=>*w=hl qVcyBuHC4h`\vE!{н%Ln ~wP`4=tҠ/8h!sR9JHF7C BU.C>M4tѠt4. A`?A9E -'EDV҈2n)<1a!yq/IZhxIE4[d`tqj#Mv%^N7l㏏A>;! 2)u.r0N||K8Z}m4 -SaerԌGxw|*bl Y7uG[ d[ldBرBL(tvxx44& 4,QHMt’Ґ13l(ۡE2l i̯o4#LYVPJ%߱tG C5hz`H#` N\v(R}t+! >e|ӐTc4bDOm! &-my@~/RzvCUp(>i!0._) ,Fad2Vi ۡZ ii܆L0f%?Sp4۾֡C8`)SdC\(Đ)`E.E( nwnv\%gKe2KA'ɐv@x!;5we*ہ=s({U=~_X8 YʥdS䠻BBG H&VC:E/{ ɚTJ\x! }D/7 P{$4)/+%9= \m/`^Hc%(KLFK +x4Puq.gbv:h"@U$**HK^^HAXtT,1u4~4Ņ F"\=pk֚3 8dv*?Α.d'H"ca@Ai7i0{J7bA%mXT -] -P9i463a#I E)ܷM\QH8\d;~nN%qny5\1vkE.XJd (o'ۡ:,Mp&н[+,ЊȢd˺0TK[/QI9 ̝%1ˊ食b *ӿ ALV S @Oh@)F}6(H6TagӆAxyx# ϸ  z BXJ|LiW3FB:D#T`1,hs_]s:8pIioʞMl p=[{  U<Ǭ=EҰ@P5_ <AL1L.49|x}KAM}f%n|:J#7☩/4919H#$ 4enEN$Ҏ4L 5% -Ҡ.ń4~]R"_8}g4~zC]?Xgꒃk4q[dKp". iL&:a?Lttihtkb;]2&^q4f]mUXУ-+#N18Sb2׭X]0 +ŠQ.YB!Z('Tg?yBkv[ BI(J~4Wfm-k8ɥǖ4,Kie‚7Dp Go(bHH-=PҀ+IPH#35K ݖ2m! {}-Bo=.Ƅ4ϵvC |! \BvZ.n@G ! ډGHk$}c73(av)M7caB% J6 -riDCnN$ LCx 8 iBO=f{X9-M{&sĘ2J!@(1ߣM)q.nUWזSmS_4nHf>^1N'Ql*TU&F[k5G GllzB{pw˫8?W29{7zڧ[Je{ #0dl> i\pb -`{C7*R -i (JTd"z8e{YuЭux i|);B-!3sY̪o4 -ϲ6¨."(hbu:HC) C2! sD!z.+S ]f|A&\4^ifKoi0 RMFhGiQWTÒ j"f74i]Yzmi%"o}n)4TViE iӫdĘg) ߅[fCt77éy iЂ,U'Bs@'8`lCgn?^0\OyqnH·xHHN( 9d1xO\9!3Ђ4lr=FC4[7h2\ʮq![mF`R75E]<(u}V/*!@, -z4pIeeoְ롭?-z2r AfTjU揆 !5@^X}_%P;^ -='5"Mnt/yWbp,{{h'HJd,/&7t5⬬x{4#b hX ֳKNnHsOC)\aݭ"sFHpQrL|4Ν] ē.M/Q¥{4Pxա|4|뜶qwټwhGc$(c\ G) T>YF|4&vč'Mk*,ډ68SX7cVŽ( n#3SHBf>[B.*FXs+g51`S_$+\>s|0-np[>^6ۈ}4ji3殲p!nQ ~Z~4_!һQhJ1 g 8Vj]9t90gv˪&ޏFJGct^ZoBp5+fB8#-(h&&H<ˎ=4uE8<IP6Wb/ZVRhF󀔙d y$'1h`!V?]y3#FeEdz61;{i8tl.w.1!Gn- G$M"heS.hXxސ6pјG=^#9!2 GYKg+:"ڿ|m4GȦh<`BJ &qfeN|D K -RA:HC /ͪg Ou Afj5 42ren~4YarXpe~4JsGC|Z> űJ.½iMGMб^nԢ> 6(lLΰH4Phx$ YhWfQמ6 -WsJSH*,c aEK|4lDCTlG34H3@c:BVU5~4z+*49CCYTFN88aAs8??u KSekN@HoMb_ae1Mn:c|"]ٿv4T龶 oq^K폹qa %h\/\303Z@Ex4v5x46L`38L`hȡ8Dlӎ0upEF:d=k(0䓿ТGƨl SYh@p%\ MTGOQG#^5FMD6oq+5fy]AE!O,I6 I[V78nrwLbȥ|4=:}xhgـ|4ϳ -sҠ,CoP<Jo%R..#B`pH^ G P=peJqJӾXXNzăhDx!`!6H) cCyxG_W- Kؿ3% }4Gh`PeAe|G!/+bWJ2D&6cO 7N+^>\J6MX`PZ|w7GhJ@,ê 0b1(VXX[hX}`G#Ҕ+gufw$ra`ϖI*r |4ƧFep2G -,wJ5OB>lw< = dc릻91&"IuP^KˬLmJ,52ՙNz}6 !~U3VMy4\6yҢXMGHL0t1khLKWzSNW -2m +DƖ~rHCrmhj~-/ Hg{go]`7Nz? -._4?/"Q(V[]ytN E$m92#Ђ@vP}4$\Lo;4qbd`e 6hD@L XSZ"N)CΡ8<邾Jr2ՄBx+0r{4xh`Q SO@۱zAB)tmiZ-jG'0 |4Nk,ؤi Yᄋ㣑ѓdSóh DlQ z?GcVأ,VFo3e -{7z [y4 SzI: *bṛ?kѤl#%QʰϩxiRr=CSLee?&K#Av%ţU$L&6Eg:'m<[:"B (e} WY0£PohL>~VBiyh&;5_hYvG3p)UzF%bl$*Od@БthuN& t7&= n]"Åɨaԭo:VG'cp-FP!^n#Xh]7 nlbvkD!d=ߑoP2)._ ,@NWghtWVk4)h໱Yg$Ϲ:.'c>hT1:?Kjv_"CWrDWd"GcJhT}S -0vGUڱjFҴ,:׋pdW&ǡtp ؛I u =z5 -R40gJE=)toh^}_L6aqo2y4+vcAGcޫ]<ۛ> =d(I=ݢ0r9_[/;CCx4F޻qRlo0bGM=v4Vdc<AFX+QJK؝FcM@Z^(=Lա72= yq9,FCf=}" - }p-kdk1VUАFC’;. 3J^͋0ZJxP!gQSL oq#ShdI:_Gq檭,+aA/ v:/A)CPC(fIjB!NbN[$?sk4mɠf1g0 0KGZPGrW%~"dbh6`v$g h?whCu 6>Qze,+t x0a1w`Ƽvٰj} nAuiA.@|$+Ϡ}^dCJ _O+iج͹u !Z{(6bca8=PLE.z8Pb-LX6o`wS} P:GjHS]~?xĹ;QӂBhۢmb;`~!aXor @ eFc{4 mÓPDZƯ, G03'h9m/?x!bU# ij{j&UuT s LJ͞YLh̜.pdܖuh nhۀ6BPF?t {ȟhf8giRCc$PCf%oeP-FAnptcfk,-܎F]8+XG hRh$J+DO010h4=pZHTG#pw4~(sO 4 3bNe*'>3VZaF((+uU -5؅ɞ' $Mˊmv} 4w8NZ!m7ϻ \eY|X jPYɥO"tKb$P''zM%HܢآȖ8v -ި (A܎F}; `\_#J7͢Ć1tGNA:ގF&L.Nv4ϓpFxByS9>Za.SĠ(G Z[JѸA85K -_ݮG+tq?1};GŴ^v4^1‘Рh|:\͘zo0po6 K;U; -ŘICV[L ~Ŧe;*X) 4fNdcnb4O4'Y4pB 1f}h б^CC Jh8h,4Hh)3c |Sg>o JpՑvL_Bt6gH ƘP0VG0"ls􁱟 ,Y#6rY0 >ɹT!0P0`39:,FЄ/7FZdbC+}uDCRWlV)M_kh=HӸ4$j;v[eC/޴vN;MԦPƒn6q <@Jj ХvT`S@76 ?ZO.jQP??R iE)fJk)6 RMIT\O!cnZVGxq,$,rRz4&@]9_iD -D mqYk/+5"m/._8bȨй$"`&^v/֏m/ً I/tZ^ drb&2n^XZ0j/7qxqPM-] zw"k'.:&.Dn* ?E[]0J" X')TM\8Xjl1RK\$5 WO @rZjシŴ|YWjBP: X& \-R@6&Jv #J~n&[Zmhjmjhfj#vb4~-JfkQԱjFxEEPըrkj[cj˯P13$-h-B'ۂ&gj{FQ΂tb5ABa҇YX(d, SndUadqud!yXc8uVj:"l&2WRlXZMsYWstaMQ/8XtE]amh,ntB"#e@4qƚb֊EQjbQXXP ScD,  ରX m\2XkYU1X"ŴkgWGdg_ -R{i -IɆ^g -lsfkmTG.|5MxC,{5ل5kCXīW08"mLP]+8^]wsPxY3WjC8JjmXK *';@hDkx 3E96נZ:^nY[i2^!ETU9eaUk߼Z8㮑WWEkoZm\WAikg[<͵\Búĺ׊+>ZB'2ۊlmŊEB?l -VV4;icZ!Rv~̊dœHwre!+ r2r$Cal*R͉Wam]E[ yxA"Xjf{jˇUT6b;fǪ¶.:* -Jwpxع-"- }3p#TnɗfX3݈g pT'nGEʨ972E *XtkYhޠb}MPfh?~v7"=pS[xS@ީ)ڼqoblu"!-o@4ߎ@X7f@L!/|(p]MMp,{\hwR</T:QR=)IE")F jcε(8:aĠ2ȭAJ3]$oK.E]LR݆As|)|H ([ -ɸmuS8OȢ~n}IO잸=~uU:t'J5'.}'t'R[?j'N'A"B7jVg{VU0v"K|:vꚥ0N0XUw.Vu:Sյ}0Su -If꘮0^'鍨d˟NuӽmI w%u9׉` u:؉x u3©ìI]Npx-[ ©W_'ೌ+} o!8؉3`鄷F'zNĐ -3z%YyJɜ@5/"{DUhT330'0R]܄(y.0'=T7sV,^]uUmquvp2Sumu9$v7vz8NǜHig 52'Ff >-ao4'̱N #{;ID -w͉N|ݹ9!Dq意~ޕp\l;|VsBaBC!6'v -FUIS|SVt+Kys7={w-ZpNWйջ r9$>%׫,]62!_8Dw!pbU&].M;%Mx7x41UȫMhG^nhD Ll<̈́ЋX .^2!S&ҙg(L:$2L@yEGZLzĄ7 zZg/wLdp k/a_b^K/q-D -z1@+QTe%"[-j%60==%(r%x)@{!&,%m ~glBS"UBU +%*aIGS₦SŔPORTKgJ bgPO)5O""T8F= N¹&1o$D<&1%%qHQ8WIQLWH+EˏD$eա#  Q]rI#rH$H ɟRt_|"#m,"n=iD둆iP"x# @D}繞CpCteUO!`  =kSJ"\d BV.Cz!Rts𽷳 -!(;bEϐ !$|EJ؆/ !!_χ!Q" _i>}V8!6|e:6vc[+DNtA1AH5} Q_'ɂpw=K -BtK + A4}4"uqBh@!Q~;?Jx=I BaoDXH O ua1 ~Oc{z)#~xߘfT!KN~>{R = xJ<~W(tDh3ph*#5iu@??Я5c7gh'=OC;PKU!Ĭ_A}[$G6ڰHX@faLÝ7B4p2]J! {z%` -X wsc~%x@ -d4 T? 'I@X[ s^Ni )@}D}d6Rn"$ɇIqU+d1OP3{`g&JUN,旇F\yh!< <旇l+WMpKvGv4\u OB2H0)^/֡ V2Tu:0rC NDL=t=ay=En'ecs%<)N79ħ9<4? `U48pFK9pɁ;Ɂ bA Jl B2xp8lf!EšrBTs8D`89 Qlb.% >2n nA\Z2؟`؋dYݠU0r`1BR%` I'|VDL td͒" & $ʹ3с4( uÁ,0x s l^"=Q0XASc1(A`g)& %u\GLryt霟r:h1( b(  7DJx`Z V1 1%ĠnRq47+!* l ~/ap2 04 'pA ڂIat\ྃ6F5ٳ+LZNK`K% X2Z2`pCjAq\n  $c!uJ qCs0XO' \/ m6nE݆k6$ &_ ޣ ` gyj6Y65!ႰpP7k>޸w:m AlFk A2e~ ~5A(Dnj8$5la=/wNCpua04@ ֖ - -K\j1Ne(Z5hjD_J+xv?C>Ý=f3$:h3t 383 x E䋵 \B V/"e e wA2aauXv (2 `œLJt̐ #2#C "}em?=t]0Z0 Fb6 -h `tA(*-_ Mv ?oIz|'d2V UQ @4!bxR^02 .( 炙K0$  M0\t`Ћx\ [ Ku.:.u<8#L:ȥrlO pdKmY-._x({/t. ɀ]p. Y}]Y_`5_ \d5/O`_^ezӀ,]0&#A 0Hzw /\. ]B-낉u. -u k 8D[x2pP N]p@…$GH U,v[taE8jŮt!LhQ^$W.NpaQvX~f&?r@t"  U; kot] |`5˰,=Bs ʅC-\p2.l0pH[ cd--DN-\0BvjABN J \0_@TЂX,D3wY * ,<GȂ H q'yXx0X fW:悕+Dl$+tҭp[V"rAP)xVxq*+fP?X*WAv=W+ - uPJu*O> -٧`I -R!**PTHՎSAO!炖u -,)L Ma38]3VL!B<° &#QPGacp3UTd$E[:d Bh܅-$nP8$悦(8'̐O؞0\zR'wDmN0k n' TƧY `1W. .\]Vv%Bu'p'Er'${[\HwܨûlН`k+wYg`t>vK.'g ,@< ~*f@Ci86Lo]E9nj G`,8Ap P yM낮4aoh q̄UJpd8Lt &~ \/ r./!Uo8]K%9 c}%n]Pyu.A^%[^p%X'-0pt$^PKSv\9'%@H K_h%.]Yn]p% Ȏ t G0WIٹ]0%/.}7%94k-gg "0:wݓ )_WJ)˕vE..p%(EVB]DN ZMĕILtwK{`Mw_pr%>ɿ`AWy2.WN═^ a`W _ <_ {%` )`PɈ.v╠Վ+tС _TjWJ@$&GY9  Q0 -60HN&c[ s_[%&lݬ$/e%l*Jؙ:*AA.=%t [Y? -wPP"t$$\G %a6݌(誕TzPJ1T,o:JBx= * +t+h u$LN0( .0R$ \u!!T$`!@{GpAގ0 &!A?Ct96`{oumk[E4# ~FFz}ʷ!fJ #(&6 -8!лtwz'igJIݗ*M)! =qYذ赋bӜL9B<8X@ @,!  d A$$@B^F+Oz7CQ6tr BzȆ&J e;Nnhp bHA (48Bdl)fßSd2~S%4}~S7ȼqN(Ұi|kf3UXԣ^>Eޓ;:SL"h2o@>߁dl Ul'VXGu7ytR:訵UDʫhD7D:;>,ڐذ)MM)> #>1Ԝv*LۧݼV>+0C}^"2Xlue"B&L@Dx=&T\#" ` tB -D9^B)֌2$:3Kt1Q8G2Dfqfߏ=4s36:tqL3J~=RԍiҸ:^5UϢS' *qd*֭L+ -UYNU5LSXuHV%E#k/;aE uSF s):e o^J-sNu)Eb =idȢ&͂z4i>-Ȅ8CEetABws-^ZTY->m,B=ґcWU(Pm>tRot|wao7~cKɈȕSZƲEt)|"S?]؆<V/^M]dA - -RA4~8S4)N2?SzMD< -EۦʯXIͤ\hNAvd,z<̼`t4`1QtCg͇Mw3&şb\t 8݉TWq$mbELDCoکn>iu9׍5|u7+}N1ewGڜ z\Ꮈ1}VۉЇi=G*ܐCr87&5#Cu2F\APY7z,tmW8V77>^dr%U}=*L S"WuTLzLUƦz9m%dq^"dQ ~fA4@cnXʮDeP2}tړ౪s` @SU.j<~b@sË \O1:5.ad -(%.īe}7tc{Ū|۬ Fiiu"<1O[D;J.kt]Va{2Ei)b`~h:礊!ndQ -El: n|jҏ1l&ܸIiC5gehZ8p&'eRɡ'O\ܐMw2sZNn9d -I8Q+_q(f~s,P^_(kȌCj^ -yP *.ӤɑCK+VP Č\JcͰ2i1B|XA4f㡯NQ& @)1q; -zUB%ܷf\F`@"Ked%XQ){4}F+*{D;""=q#K\#Ӕj;ОȊh6g,3͛+"~ZdxꞾ^S -U4['Qa/nm~SYm_Liܔ*! ̥#3c~M(Q֩t瘟UӔwJA%ZĖ-r Ӫa-w` (;R)ߵCGndq(Q 4zp(>.(3Fn dM{*W(FQoS6UClָ3 DMooLWkjU+'Εf(sg >+DxvKuVx2D?#D`b m)]BA#c>mFj ;CQZOITJhlBO0(גgIJE3ge]s#o if3)ffFy`|8KHN2蒾 |2ժLaKV#k̈́%ZYezod#Ƈ\޸jQ,!cXj M衄;'F?`6m9|ܼ-ԋ# "p3 -Mh:I NsIz -(b Qui{u yp#2ɪsn8ޒmqʓ?N|X ʑ=*ﰔQ3'(ʛJ `ّ) Yu% B.7"xy>vZ' -heYF'ly;Tޏk&sX0v;Q]A:# FI{GSh8ϯP(d';CxbO}T, `dwzURܒ&\ G9j􍆱K3(K+{Rj$ĻB&ude7V B-05WI.>Cf-#JLY"ߪ@Z$jU蚵N9 |/(g>m>z]waCcLʺUl q[ +J1i5*o(C(]LG7 8^`$J/LJl4G$`)xF0Ir0墈@6bA[\TkrbKp;8xq8V:ގqg2I0(fm=hg˧[X]Ex6zktJ]ӑ56Ϧ_f/fFi):dKLFiǪ̃ڱ^S㾻vÑt}㮟,Akǩ^,%:=U_ ue11%##5\yUd\؅E%D -4Jp -X3VF5}֧D^Oq݇b23~/ƅ?2 &Wݙc`4I5[]\,5]0"!lY @DiWiԳhNJdԺb#2 xPچD#xk}/UH12QȞ%p~ilx[ 7^gݥ.C}R4"pmMLОз0öe -UalZz6y)E7T7txzQy:TYF7BdcB| ?lC/aF -<ɤ Q\Bl~}B;c>z}h` 6q:_ E)j P*( %~YXpq -hUN1J~bC QXl!NhMis=dw;׼5iF P"v'@JiB$ަMVY4Y})\__ޝc)]g|Ȇm̈́ 1t~)}™I*\CP_@?{fs?}xĜ] O|s Pre ,Bn$$0s -=`,P;``:mě͓!ǮK}6 DF3r,Ћr PƌQ7w:'kVGOCrSob1f= {0{" yYq`A@&V‚HfKBhS_c!yHܫܝG@rƥVB pP@FQY"AFS*4z0d\o~k4WS*,_W *LPjemo`x+vߝf - _*ۀ[C j&o5(ygPhd|J Ć3`@W:DJUdl ЈtbNzǒn?OcG-9ލ?A]h4HPLTt3 w=9 cvh˨ Zd08fO9a+]Ѽ 4vf̣]}m֥=_iX 2r@HjДj:+u~>YUy|m8r57Sָ(dȄ/< -Kt 1ozy- l- W8NpP.Uf6-!r֙HHE,*ԙF#UYyaN8 ugW/u+x<}V?Hua6!u& -P|zYX< %jch +IO(.4Xc!Ttګ?ȴ"se5s. -uϝjA1lsP`j2԰lc -66݅ZIY(k@+k[b'TaKr Rb&C-\iUiA40F t45P4C -P 4BP 3hB0i%A=;J ZeG:C hNp+_2yN+zx&~UFxڑW{2_O¾bHW6IFT (ywyPHX:QJWY_1ta_8 ehε}୺ZuK pCG -M/3)LY MjrdLq %JՀH=7'$L$GlF[GDG$aE#QmrraFee/YA_=K\AiiVyf<|g8>H+c= C[>d$.V"9KФ~g? p-hHaI#26RIG0b+P\cA -ho">1GjvWӨlWE |oϽX7E,bH=JǏK `i)ʹ?;_)y&=3,O}mdYm|>U8䉾闧,ZxY)|+h2'Xb?pENA<勔̤&%Iϛ)u߯/_#4SxG>"@22+OR#"PR0 ru -~S3J@!DX hi@U¶F/6LW\X%}8Εz,a`i饂R1({=Gᘝma -gqG67n}:l> ߱:ez;cuQh;  - $h鬟"%=M@yaVk$AȌaD;;'olaZڿoȮY Z-j*Nl~Ϧ[V ܴ߃ h^InS)l+) t(3$<.PҐDWzWxX<Ħ&nWDĎU -k}C6{Y2"><~ -J$D!rLF.n&~,ҭ\ ]kFzfkJ(FC^j -=*|y3,) -*ͿP:-0uRڱ9w|wZg+$ g# B( {+@ -GS:L_oA(7>m{$jZ>vrtotqЬ9j4aX&wVRCbzb,vf%*gZH|&ą/܁ .=&45Ա>B] Q - -# CkmX`b%wK .`4܉ -%?2:{tC;GcdԼ1մ:^/-~*,.wއ~#Q00i1HYȊ+ZL V}ꔆbRCQY| 7? Uop M|` ?/Qg}- ZG j7Â/bd/0_ANd]׼Þ]-1MyG^JWL tt5#bWR~,A(ͫ.oÈeydJ"%G-xtZ"ptpĉ)5 yw(`E9L;}{I_zmy5 )-aq@}NoN1n0KRB!RTHIFJkQ!y?Y#>u6~2O5s&bӮl4J̪k{YʆO1p4q1DIF_F2kJiِDʍiͣ˚q (%}`h8⪫#=c퓶3h3c>sȵ{Vn.DR*d,[=6Y0pp-A ~f N)j)gѕ lְ8` -}QZiґH 5:MZ^A;\1īĕƠgvwH~h`!|O^n MS" @JD+Ve`8|2pG#jo%Du}ns7 lQ\`;iC πҴrsUiEg9.mN@-I;,_eS'oI,..Q "FiB9c#na\ AusLk&D-ւ^q‚M>=\W!@ Z堮;lPv| }Q[dtN_O߉]hQ/Z`)3߈XIОd\ #`wJ+C=ml!b@Jن&\Uh/kDzʓE Q@u=젍>>>\#k{Z4QZ,Erk;$3>J)mA숩O}~IMv/Q71iqi(NGhJH݇o5zI{,%E}4HJYӧ{P}$MN w9d@cM(X#)oN@)vBm0V$/71$y-\"DM T8T,|Nu#5@<=\׳N[V-+Dz -FkIU;3fС+c~as/3^0^b @]  \) 5,!{-lPWCq}ΒٙiCFD;q@,Pvs"JO3WT!pF = -w\3l,aAsS.1/vS0ghu5>9VmLbΦD+`4շkz[tO[r$%/A3@\njo`mTw/Q"b!|r8it~ty4(%ʶ20(-%5=M `{w$mƑnJ&g=\s{2E5(RMnnj#uחL+.a^ '.E 8GW5tAG@Gqdө$ʤ(pG7SWZ%:+gO`}Ew&Z@StjY^Cw 0H+6AB_4V!Ү'!4 xN#߃u7ɈICܝ$O뛌qĭܬ"r<4 )ҫo-!s_jm!ZQw: !PF. ȃ>>N-E*yUQ -`v*4ڒR!]|scN|C:іv').B\-ݧ&RFV O91(׭/Ľ6`p ~k9Mk pحlZ:j'v(ie@ff&ԕ":v?*5 @Xq^[u Ee]:Vw5ro`:qX [_ -|7]@4](Z -U4@Y.0ÈmdM:>m* j{_MWbcb#V&?:l N~moW[ @̆OBĂ$U-Y)|v91c5NiLD\`fwqeN;1;ۘޘ|.R׿#U/ڠMW MX1}m|pOu9~g>X,;m8f79őƙV̘apnqCh~mvӥBF7( ~Q+{2{]m},ǤUm ? ){!Jca@xzJ B>OAdFc|phԱ-qb gnCcc;Ҷ1c`R5t;֗B sXFPȥ4ᝀßq,^+hJzͅXv-X; S?Fsotq) X>c=ƃֆ dyҰ`?1ȄǬV2]V?f(y  dǾcAM1cѩ1r@zF~L؏4nK>~~Xhڏ+Jd%=>6MC7 ;f6"ӞEzƴ=9(Y4ޒɰL&Mnw2IeN)K)uhɼvگYv*CfeΏ˒˦s!^{a&Y!̲Bˬ!63÷LF}^j6SSG #PG)koXI4oipXg&g}RL\g-{jݒݭ2A'=Uiptsh@* .JCJJA*Rd.AD ::*.A[tSgFҽeM!H;6\Cit!Q"D]+#udFTViPcgA -^r 3m%($pHiBpݏ躀tc) y\|l )X4':>loؤy뼺uuE4;їuM<*벑) g|J(Qi4uc*YG_!:ذK@>uuJ㱫 -;dǍ+c\VE.Li4zV'U) zgoJ(c]{u2qvõ]Jzg$) dS7?P) kYشŽ.) Rn;yoas Wr{֑h(KR7JS'Ҹc묋J#k]Ou&qbO6κ*k "S-wΪw]JaN}AބmOb 3vY ͮgWj.cvTjWCۉ4_@ղq4IS+vKR [Rww9x'ʻޕ44vsZ4}g -rwJC`JcbXR)Ҍ]O+}]THR_p pV > 0חXU u(V5H6M5J* -DE ywXv8x7xR4X(N(_mOҘS5Dtg__AZ;|@i(nEFFi| (̟<ZϱLZ!(Bx`~F?{xbWB\) ?=JipEG J^E^-IɣДGW^M<) K-e^i( }<+|@} -GԹѣGҫҸɦORԎ (@Q~7Ci~.oX4 !DiB',9JcclD <֋NdmAv:y6zlJ -|q덠FlKJoY/a͉#c} ̩Q\"1(I|Nqϧldr|T4!ڤn4^ԔI#pizB2Ӥ(N1${14AnI6{8h}4{'1JI#|A籗hyJ+VxT̞wV2mՒb\z@ICA{xv՗4@{p?{Н]/i8F.=%Q嫵ƙ4D>gq]Q05e(2&/송Pdmo_@h4&od6ܤ!QdQ|Cn444G>' >lC*הm/m#ePJoF* JsTTQ}P> ?T9yy4UȾ̯ǜ_,Gwf;l̏؝_ 'iS7[T~@icdZ7E? fk.MoF~J*!w7<{:N(؟( B.I-OTҨVP쓆_j(@_H`P*ն4 iiSܫ4c 5d>4 74W(`D )βor+dbHòQ@0MCd<:j7a0A߆CfeBK< .+?fh4 1& ȰiדA]ii 40 ocE  #~3_6#_+0H|@B.)[+X!JȇAG"!(.تAϷT!n#X$Շ,Ɠ*{س ªbxQ. J BpN -h~ld]P vA~uA'kr i`>~BZZPF!Y V!6$ưWѐF|$51R/$LRV -B]0 !1AÅ.H XҘP/IbA*1 %.^H%! m\il-zgEp_l F -iTf[=^HwAq󨓫u! :BE TH#AHC]m.ᰲ`킾 >i+q, b ^y]Q_42TT]V<PJJJ7.8пDU]Ga`{1]K4^8 8ѝ},BnE~4L.hݠLa.xß |F,O]hLt4)wHG4 -HFT5Ԛ 6N̘ j]*"Ұ(H#g4B T5h۾Ud%R!Y )H#sg]` e4/YpL/A6`Y4<@+Ay Hc1? i(dHCW(VA纠N ^ڶF FQ> BiuAH ~]r(Џb#hRҸx VGC%}0^hFM s . -e Q|ܿ.)~3MX ]hPb1{4ѐ= >v]0qR|zÀF~`A! ~!)/hݤFjwx n?xAX ;pd -1i玈]0DUnGż qh{4~Gd&4R%r4|4Zr=9q7!*@k&1-qhT/Dq9bbfN>b죡 r~42 K [O/XN -H=M5 K>&#I1_P0ˑ8ok - Z Gz4BA=?G= - F`0 - L$ > |4ߣA T&hy^chE*; u&ի. GoʀAJ&g vѸC;Lw:v1)WZhh_\)%£Me#uQIң19`MuP#0hׂh 'Bha`P`8 j9 u bq1wHƣq 6$ YU]x [DK!;Gw4@F֦ -Tq#9ukח=|4G`HxPP  =j |) t``A ^y4^( GK?C@M Sl`@hPo~79jSTdr Uh(*G.t8cOV@h t7maQ@, 1O!`7 TUvFml45߄Ik4┄W$ pF0XeFC?m4$L`FyM2`F)081Z0` & Z?,/a6E$_q apWaPS7E-gJ\ RTiN&0j a7n48qՌ -YLpy IGԴq 'N6K!?x4;GCF1( QĔl6Mhdu4aY@T|;0bzeGÂ0h`pMIhĈCQ# - :G0d;[ .NƢ ' xA aWb"x4NP"A;< -% h +$^׎x逖cҁ qG$ VWE⎆qЎF\l`0E,:jNWFq.ԤYQ+f`cpB u ]q<˵"ݷ Mx:ü4UNHBv4B@%./"Q,}ŴeGcJo40Loh輣 ,~(u?,ʹ#|F~%*NBS  LGx O׶hׇsˇܥMDQnYS)9`FꞂr @(ҶR --P*5P"}T֎ƥmê"aF0݃]lH *&4E< _Pl/d%v;xh:R衽A)`jTP} -Q9X\UZKGCT4;G?cZ?#Pјkh FQB;i+3_pZ0}r(ŕ(´g -p]sq0u4v1#[XG4(x) 1ANq^F2P 26T!RT@Pc5lRSbs+hi}X]Spb9y" ȧ,j!ETB"lB1RGDt֌-Uam;WzyY_F`/R_ -23+QqFBSIIojYau(W[xw&'#3Z>ad244[y fG࠼Ie'kJ)B&Aݜn^̓`YPa% iJ4[y\ 38̙N]V>442#2HmMPUI?Z>+9iyD3;>JU "qt8zm1؊h_! Eٴ6&(& {ef:,h:!Rp-8hMoC;WdaHNR6K -qdT< U]=Ab -W$Rsu\ m;Ƞޒ&11\⾺]"nvd8$B+{&X{2-QD>:{oPA1֓㷆k -ݗ|].JUdP'1EqI+#JNu Gxy=oNT4pT4R/ah5QPO@EGrL%d 0KuJgi:2#Q@AVy*˛ -:f\#Wk]cuS<O U, 1qĴ8]R\Cln-4&q7K/CxHԇz+98XO/L%, cBťH.(rH| )U1YLdATLwAc'S*)R1I|$2EsgEІ]0k_x2H dD ӄa;KdwKBrKRSOڪg5U E6sWA! +LM\Xg}CEd4垤DsNԨ._7& E?l 8?H/pHHrRMy;] )vKL24!!!OL*UOUOb"`۲ȇ$ڗV/!bpY{ p2 I5\aM/?;FU7eEJqGCŃk\I"3CH%-j'e{Z~YlI竪GO",iMHmb9QUYͼ vnop"E/}T#w82APHkTHQMJbL(>9MD!.($0SiLˤtbH)BjT1R[B*GU-8(ƂXoi3 CQbNq% GT (^F EPa5* 2 zysdvtjr BS 2SKafJB]0:#)V2PaW${k&+ n͂;Ш-&@%kC,[}lBe)^0jFa-.3a^WULv T+KX= -kV>0@ x(Pm"vC#vZ3TC+D'MhH)^!!)|xX$~gLe $7LH.!ʘ1T'<JAP "VUtdR5SDS'8DT^HH걑c.Ҙ2ȴޥ&ލ ni9.9ifώpCX!2R⪼ȕep9)نx yR)]:W)1|j#/`6i & -o9AI$pLj(=/8:8[iYYgc{; -\ā kۚuڳA^G>'9wb=A-. {:/% v_Dq2)*O!գ2lCn s>:!hd;Z|".Бt~ݠ gKzv|z?M=ehF*bl܀s7 -,pWK$1 v>[,gݴLM{Ezwi$y5;\TȌ6P C$h.E]L6WtQA#1aOz 3*HHG槒iO/ -a tTݖ46F<_F7qku]#(g,YӄjB{,' m ]g|"XF# 1dB~/5Vt'?ϖar kCvD3Læ -0vAB2dQ~p*=⌷ѸN;mT@??X,P`n$:bN<ı #tZ(m4u{dܚ>o0gWi^׆Ƈ7`k+[hC)N&nM\meu.vYlCUJN-=c@ {AC36?֢{^& 2xH+ITU -t5wCDC8dGd1#o+^36T~'9m.&*4IAsjOdas>E a{ ADDWcF*ցF>=Q>o)u*EΔBv}~ EnS~3 -9ULk(Ϝf .ˋ mgf1;s36{gҲA?ov'޶Ky\(?ޫSy甈>M:GqP桢T24t LX0z6A]>d#} >D! *lU`oh4QIӭ]jp.IC"U@ eBRеuHVX/0>&f|vq6yfAp 1wSUz0cx(`>oZ-Fyq^Ig:1LVDt -Aƣsґrft^jUc3L"hhXhH <0^oX%OΝL[! -+IGʪԧl/ O[dp;d,'edJ)F.q/kKUORCHXĻ}͓#gcD uv# -t`1AstĄwSEM'q7 %!}I%,X()W5"ys -k(7^D,H/ X5ߘ D{e:ݳ3,_-.q+S0׮;AP$_=տ0#fD|r&WRXF(Xqp*]Y Ρ$NG#zYIT0 QkP֠ #u=ۜP U7XȻpC[O+f_l0OQp^_MrԗEЧ[$GWcUMl?fH0ڲfJ.wM$6ŗ!alΜ53F_&{W - -`yP.ɬ. ,tUI.+ !B1>YꞳ uE7ʶep)Tf64}ŁEKȊz3:$ӧlθ=:E.>4 )ԥkfB[@+ ե5'0vÍbA/J -Y_ VFןOLT3(z?c-; ǟo}l=Gzpڭ("nFFT8Bk~.@ݶ2qJWXnpZO0l?rM^3.4}L.8Y -U4*Er jS:fNHB$r -9?p7q=Rg:.<3I.\Dn6JXi\}UZ'<P C2iVa* sW1)sIe@v3(]o+/ѫΈ.,B^ogb]=GomvWs<כ2KBH݇wh!z-]3E/jU6:  8E#$ f㔋xZpވS陸ff|KUSpubBi{5m?4 -meKr9`T3\?F q&0EkTeC0/xZ DXO0N`F-qDd@;G4L1 .hPAZչRL)f=fcBH -.(f "81&>B 0GBK`M[0єDP|_z{0U갇Ӭs'#)¯Nj|Z.$hۼ> h+|ڏ8{G\D y5@_HHL -ßC cTWSqG#)<0?5N҉GYCӋ5W{s!B[-DG!RDR:hK^hO O*`@mIPюyT-_F[18`mȍvѾ`oeR4L)2No"hZkUechՔQ-ZͷH-iԛ\o6fĎRoRZo0桜괿9j iVϲԄ%ή⺕ ޯK5.JM}7pR;"ZIMBe"PU -J9.lX,03tA]4AF3 -At Td-|5S֮X<n6 UCq/OW4 %]\ UGҋȆJ D97s?.*7 N~:Ppxwo{xbeLDC'Uhn'"Av!PxY5@ IE<#Bw EJ#^shFYAl-?8kQpaRx0UM5IY;du##$#oQw CE>ӽG['*)4xz`D{0SrF@ARJCipeY< 9҃6;zyEg F*(Jvh; tK$T)aav'CH C͕uuy4T}_{˴]KxwAx' n7a1t۲jV2-lqYfjjT֢AN}94;Y$1;bBoXNp]ȾzQ:|R%?q7RnT:!o0(CԔ@0K 0]yuzĬIpvg;gYley[B{ٖ,ŨQ(u U 'C ʇKH#T :| -4yZ8Vh(pw"w -0|G -jls*F+ĠHcp!K,i[ /ͅ]gPp#痕dܿ[m9W\ak)Gb@@1x4XgGguc4D5ОՅނBi5]نq Dd'HKD=A>pDQ$О t $9dw.l7;Mz9My_딜yy.1˒g4;Xꙛ@QHltzC ]҈niRNzH2KDBn#kW#a(h! iIw@^ց}u=Ou|pP"]RݐTLtit;udJw'F)UWwzPQX uеbpp9^k)Qk&kLk10_BGW +c3YlNJpaȈ w L\pIpg۝ZDgAXYpƻ dQRBLs)&nѩ wuHlKWT?!~+S惜Iov0j7:Kf`fƠ'~Q_/3ًHKu, 0.]>a(lGzNXMxaL _ !`#a(\XAaVr΀r$+ǚ=(RvCaJ)ni: O * ϾςӅQ:D>4',  -AMa,4 w 4{BNC'[S`&9U~pXBn-M2fw (P5 ФoH -#k⯛Dh/~ /?eYH"JG?>8::6b @l/Lu0h5"z%SQo?f(z $OV$bdKDP?AdG?ѐ>?PJ?b~r}J+cM1Е5_;&Z_b9h繎_^@b&Glev-ڵ44cOD;β5ߪc2v8=AdpHܮev&2v& :p܏xmm>Q$5y^US#N[uNg dnJu^0DoD(jԢ s,>|pss10͖& U<?ylDq~#Ϳ8t-^ .=y}jdg݋%=-$A-ٯ .aJ">jG:T-ŪM/ڷv˽d%3ꖗ(QC{.u~-kYtcǛkmb,e@#y昕vDzlO吐` 98Q9WSyX F@9A%*$[<zvAVT#}v̈QBϸh ޻n6vKK]f(W#欵0˒l8qZK!f4.5\ę_B lvnf P;y^.˔rBH} znzuXY۠46у!sql^Mؘ>M{"oS|KyNyQ @/[{tVJ`l'<띗lm)`@Yc6{˿'༫7@t)z^D 39Jj50h(,QjM @[QgD 65&; -9Μ|Bn YPcjF#}p hܤ=`97CFpߔPV3 T#X`ˏHVa4֟`CPp/ac53QL,{jTHTy 1f\y7$Df҄" l- /u- 3JI?M׵L&}USrҔ95?m!NNozԶ=Cө\g0eivJF?N"6s;m!`D*f@O^T^kԖV.\Pˡ#ϢL@kABAo([Nyq5X z^W ^|aCa:S(1KM8p8^y35U10P]i-u>EW'cÖ`Z0PRJ$k[h“8̉ͼH4]×6WSq*?g qI(eZ{M*!.Md-dBZZ}K2™gqgӱ͞b_Lׂ{9@zb3kÞg`i\E#)\{=|22{?A%d/c#3J)"3D }$#[ǐMA痣f(lb+TG4m{]dD&B9,t1%Gr(I -26CŠݕenAx*pECtƑp۶K6(, 0fuU [M?S@~X=8omgJ,j<3DcQ0mA2~,zL 9'\f(5<>HS!l4XVTw%nLYRGƄ6 -d]~Jlߦwa"͟m7\phew~*CҀ~Ltf9c3g `fſ:ckf>m+ѥNV}y -b&nUqtb-UEq@Ue OUNQF#n,Xqԥ؇2b?̤);QȨ-/$**\׸FM$(50??Hi|"BʾF."~&:6efA;"F.n{iK*wʄ lǷ˭lۋ#PЕ?Q'eG ܮۼ --ȰĽ -2Ik6Ep { /9Ϋ2w]ݯ}9%M@*+Y)e7L^N*NPi:f?gsPʍL'햻opn2̖T7{J(T I,}_'x"5Beh;;͚s)r|Ǵoh]J ƴ߀1K1K_tH:.<"'tV)zʪ{%:j!0:-m$:?P10Q5Xy$ AJu)(pŧ)c&R咀XZ 63仚c6d<4hg4ծC=-/xhHOl_eɖ"jٸ; IS f*~NȂMNY-.LߞrOGGp2/Nx2Jt疃Np=gY:=da`ziucτBlS{ce-ø24)QըuI,@3YO@:O1-s|c$cܶ‚1DI _̈ Bps)"ܕŰ09^ѱkk]cA #pʞ@Xm%5#E & - bVՏ@II+kZJEs%F j&S$Ɖ^S:e%7͓- \~DTmm0=Cv( 2-jpEkV x- DP 0DB\=_0Lu4iw9PaWڙ' em3]4ңfs0bmi-BZO{zLH$ҹtwu%fM?LH0t%;7ڌ^ -h'8zc StdXlќn)֡/Y~=z䋶_ Y=kW zZq%lܕ9VQb'pdMWNrXCFp[wT5ͷhkG8hݭV|ez%dW[I8)sx<wbj2S4x 7oΦ|qcǀkWhn Txxb99 KIŌf!u/bq03l\CLPot?F%htĝrF!u9Dw&(G?F\;/  e{& L³[SFzfD -dKb0( @ois">{vtrФ_"}u6"@CXa/B|^ -;euQL :!:t/č"hͬ)$me BA^WuUְV3 -Kn8BN}¦ZrJy)aUYEjOѩtot -Jj1)>}<5wBc4:CmZfmu:e,PF=rfEjYW:weQ=j -5])^przȮ5Te"eh߅SBVhr᧢ipsyn& /mI2f^GJ:M|L@E6f0ў I4$s*~ḨDt{Vx6&q~CٲRNCq:l]bY -endstream endobj 13 0 obj <>stream -xdF_ӏHxzH4 ($-vi GL)|wg.ścK;\F@ˆPi觎dLdǽݩ,۷C~ gp^E5N{rӹ#y=D!\=lAplQ>m=nnyjGhA_;&Kf٪8Ѡ ݝ3\;F4B3/4k:&M.V]ٜxXH/dpl:IXJ+-:Anٟ52NH)~&uaݰ`Y}rޑӈٖSfs7T!|=gT(E)'?ì +.@ G :#WK"6BJ& a-LXZd(LFR%q䄉-< -s/!3n@7>AJi xxT.֪NE$祃)CBڜொ63Q7tnV?dVtSp&7 -P'|G35dbnX9ܨ pIȳi81Vqr`Vzİ#B_ Yb ڦRTUF(^#$E<̦VD񪒞.ʖVhX2`1有fѴޯ5H;i SUQ@tFsN3r2ЫvYT ZߤCɹW>IE)I,^@i`4;pR$6h#[͑1±WfVd.8Ϟ"[qٗ -ӥEBL(uIo/jMz,Z4KV6)ݒTMGRQaB>Gz1 @'>%I-U%w΃G ^ӄ^>Ma< qԡv߇ D-:c3. ]*ZPBuPѠd!{Cnn#o}=HBYE<㠚0UAh*&Z4*=h 3|Dm>?bokx?Y>g-jധ1ljiZ/o ~j1wsp!N뵼%E[ ݔ\8'2N7Ϡ#$~ը0&4.dJ9<@-SkyۮF75=^%"(;*fb;4m8'CԭmQާl^+Qh|ۓ\YҒ K4Z%RVоU>Mb+Ul$M"n?| 97f-ٌD#䑰 -_Q<~@{-܅o[i&dVKh -E{'ˆ\v ]қq㺍CdZag0l>7dް5Kދ,lr MzL.8 ۱9Da+V^M7p804(R& <2؋) |rĶR݁[,T6,w%60um)dl`-Q̷>Є Qvzs"%/{kiS\ELGRp4y8p$[:3T]+Cwli(r^/a#2 YJaӌ#5_' YqIvb_M1mѣƏ?þVi2dШN󗬎zb #=h%hjW'fShT*3|ot>0'?OEp٨XM8`NohG/BA#q Q3$$h -0%m@} q^FXÃi)14ev2XlTZ\qƛCNpG+q),.4C~bJ6Nj*mĄ} v9.THK+Mh;5<u_>(qy]A0b ݍm[݁FQ Vũa1a,ꮕSBK 2:vSg>`HK{ZpŲ.HsL/xxJNh?4րO5 ӧDh`tʾ`F{ZcN@y}/%;g@}W4px&zcO뵶di~Z/|geSs\êO{: -on 1Ş>\㔽){Y%$inm<}Z5Fseͯi\D`yG -(78 NjB=;#o}eX]"ih >H(7u"(rxHRbh߀犫\<-A2H^4@FWrׁ LD0y hC .aneC>4!/M+}.l+ӣhZ!~C924 -BQRNJ)R+S(bCbF6I4Ks0+V"D a{bQpϧ7 -ٺ^O F)>Ga'x`tr~5fQJ0X!;nʒ(S֋(EJ}2zOBMQTH(cOGߺsFE<>JBeIrw7ŒptoxnwhB .*z>lѐ?,4!exv6m9cR^VudT̸gsaKߦ4hVJ2#l$(130bWC;kW̬-U"Akaz[pNM;*}i`%CDlO9)}#52#bGLmzbZ'c ꅤIiiªZ=[\A*f S.+~jw;p$&!j Faj>bTx0okǃ/^ΩlKp~lIJC=C GHjW'J5#J{8_󏩥 S1bR,HMhf,Ut1i^0z bX؁ؕ>4x@+H"Fh5+EH?{bv!bLɳRGRpפ-Ui]3ʞթ(epDY52G(KPv;l7xiLjn81yiIU^5`]_PL?Jk@ _ -Y(KKX.{7.|Ԇm* өJ1lh*]#QAi ;PNO8e4.5Bi_Ec}<1R08C U˕pi< 5ʄ G -F#}"([BZERGfL?K>@]|~%XPZiWfx\rl!Kd SrXhDzyKqȞ =GBȺa!FxTQDAZ:.`x4_t>idv wWyq5Co?ױ<ōBEXsO2kXRX&g!EjI8@)a+F':w$N vMʮ/Cd3tb/y$MsT ${u#r dETpZX>w+kc$CvN, ]R,:-pv$t} ᮨp5X7 7&ɵh-nKFKz70䵲' zPK -qYJXuL}$zԖk4*b)APUJ$VR -(+XoOJ#~0$r]y_ki~xTL0k~C$gxQ,]b&OT+AJ~3U8+Whcy諄G({aT}m5$؍d@zY5RÒdp ./K\ѥI>}T,{6A0ZDZU"(N1r)> J(QO3"v$Tx  qꐗ٤1GeiOMCEZJMr3Nxx+g۶L -vt*cb#: =`ie,uuL7BS;t6}4;LR*c$`_Prr Ə \6Aqcڬ -Ew1$}BOE78~O AőI B@yړ暿YENL`VRjp {]O\Dn.?&\F -ZJW@Gu5TZZ }5"N$]F~GŹ\܀r}֑^šdM™Lg.Taڬ)uz |)b6^8h$1R l.G5/ -Dh::xBPL5_5A; {C=BD"sGthA BjuDβg^&/̄KZ)琐Twf#41?N9&f&\̺ h՟셛Q -~ԒD-+;05в")Ƃ71 ˼y5OEM񠹦.](A%.>U[ %&]%Xs4roNoǩ7yJސK`Ga!1:C; G3K>r̦2?(+fE7fA54[ a4$->YHwʪהqEִ{M9~X⦩5]Ol^SXI(V0 ).RtkP}*Ftpdwun^S?mɪ1U5Pg6UNjY m|M; - ѦJRD^Mq~V97w}(Oo*؍${jyS`y5ʛ"ef3) /8yS/8>7:]-FN -o:ܐ-;'n灛XinzuȑsA")ѦMP*qS|=;mF F\7eЦ㹥yhS[JZ&RGw;[tחxDg/2.[6.RA܆oؕJ:SMw";JAtrp*M~JZUߝ>րI'fjo^WbPtt⨻t(N}=iKkbñܒZ=J,gS,u&k̘œu{ciԤ Qظۼ&54( Sgk1WG+H,wA3tBQ˟tޙKv-]\ZEդivظPfT9X%s/1!}g'Ru(]gS ly;[Ʃ~IԿRZLO=wA)xHIj 2Fè@(7]CT@yac+!hjNn~+p(p`} > O Jq-F z,k|_,֠`{=["Ss}UufQs>!pHKS Im͔Vz}Nfa[LgbZLKCSFU*bޟj)7hb6.{۴7GRHJkZ&$ƛEUOdK -l!9)w^(O.p5^ZIRNUЇSjJ)RVR,rCB)ITɀ$?GPLS_l, J-hQ}_Sa>`ܺZcspJȶPEv`0dUy¼uSb6FCaF*`v#1HG!! M'gJsW -]@.y-ݭYqg px>?jhRvKvh)[GYP.hB>OwM$œu%orbbD.c[G[o#!^"kъ" f|x+NnG'pEcͮ4j#Di!mۭh6; -U"_-u+-U$v'ūq/uC/ɮ QOI=#4<sS3UX5Y%14ye]|Aʆ r(O -3c?̶0CQFcʾWX☎Q/x-ƫktʘkGGBöG+QsJ5l0>AOLnv_0T*l="*:SfAJoz1;[;FuQ0R2J Vu!`AɦLZ ^B'iJva*XBaʹfОNcvKeX__YʃR}o|V?89QKB핒FV b4~ATH3J188{(oل~c{Td/MBPTهuQDBe\p#5/ml['CJ5qY=$@TQ ^?ҡ&6*bD]niʡS-‘ÛnɎ8ٮ5hi-by$\4}”[fqюBjuJi RBLyj乑%ZO -̛ %)bnٞ"˂^{K7/Ʈ 2bh:3f!ػb nlS8(\ceSLK?A]\LՕ>-"c]ˋ/EjQQpu>dx1@h>~pä`Pw*Hxn$ZxTAܩC.5B"tneU-gk Ǖ*w I2;dF -:c('?fz m lS0ylg(ƪ)`%*C3g` c^ '_( v9t.̗qaRߛ,#!^Ej_>*ctѹBϡvtF7hޙO$]k. J蒰 - N|3j mCVJi8f,,q/-;vuu.=KV SrAI] ƸS9v܍"47C,7LT"a>'au:*W=<*ͦX!Veʏo2gulqYW3OoLJ15njV#Mory1҄vtrћ;ua -k.3x^$͈_̕l%+`!<PBBe1>]D%K. ]H]Wnjh,mnJ9/v@1^uдN36K~jL[i GXWg`+Q}%VTD(BNPV2$NXMbT8Dr@J+yYjX)&][h;%uhtR0suxI% J3yG!a@\$S -JJN.$4njBu6X$f*LS^j*ϔ#·"]#Jkk䐚in(b]a,sU7͙5p -(鬪Pڳap%z8h>Cbg(Ϝ1 ˖pe0`+2$gyZw|a zo;+Rf&;4@E5}KMR\ĀaIKTr񖵯xW%xzbxɦ ""cCBdh ʌdH:{_e}zIKa^ԿX$$`-d.T - -NÕ<1Bp8C=H#pp둛R,|qzD'F1Wd)f됤*.kS51hsOj( f#N:)\iÐwCL~sȜmYh|%[8U0<<wwwwwwt'IwRT"-bC꺮뺮뺮((((⸮u[X k69;SIɕ*+$%[m[ -Z_E5#aZ8IFA%$WGz_8&bgjar6Qm/qr+Ԝ#]=͡zLP4Z\'n%¢_Z5Md^ÅF#kjnj``;+OćA܏YX -[~BVV^]y+ Z)&)}Yy^n Vtjp$"7:uXuPIj, -ZӢ[y]H( ōi:eH Ԑ,?O\)-=%%ؕlfbyKLz~_Jg^ , jnI-'8NNs6(2"#&6ڥm 1͠ɩ6m?WF*r#Y3(9 GQRY+JJ0PV0<糺Q"vvww6ZuVdqC{2ۭj>xipaЙXVL,Ǖ:)t/v(ºWPmWM. o+*t629OV-z)z^yW/WO GGkG=y娋.S0ثeգO_^8ۈR$b6;}:9ű)m_KFnߺjW~ 9A8`j+-oQJ -}aͲMN -͜U+* -_8LA5JJ61ߡhiWOP2SV#liWKLo ,םҢSZ5 `Z )1,J,R5+_SXIvdǫWYcMXl_ÛgƯ&%_Sjf7I`5)љnȍxXQ"ťBG9R\GaȿZhr?4\3ȡvx%$^[u3))֯~-Da-Ȣ5nYE면\iE$(-\fJy/[WZef UJz=1)i٢ OT ))xƯ=[ϕ&(zֳ0ōZ=\\/;Z Ð`O?5ڮ(zvX&v7LW(~;ZdJ0زɔܒeZv ˎݦ(-b ӢcX 7a#փkOR4MWI7zOLj6[ْCz+:o%4}Mh(?Ir">S ~Jb -{: ?(MGYq"> fR.^v#"(ɈcZzh>fH?!HxE qA&ZG1!~ UC!H )Ɉ7v4tH$ 3b/?( drdTʢ\uJRI; |H `!$B,zb~>>PP ՜a7C+E^~Vχ#ڏ aA&у AH#{ rC*{HR"(IF#4Po@D%-,gGL!--Ϛ(SQ6Я~7IBS#ЩRPt#BFNSz(<دGP $NN 8BU[Cl#u}kQ=nY_vtqD'IzUYg(u(&_Ü"^ nh̏G,R%#"+*цJ#3ç^CأI^JBy#(vC g pO "1 -H$l;"p?%tԓCF$$ QX -xEQ"-oGGA3b!0P`*h3S .5t$K` z׿;U[wIA!p5V?ԥGG?Gj.QN^QSC+LqEQd:3">O&9qAo( ԠH)@l##h O(?sS$5PŊ# AfCMiOXz$K)TOC;z CN0@h"DPSf(Վ~ CoGC*I+K~XrI@8h]hу|sbvj0=#|{)K.QFKi>H}, -A;# 6("Eb { GjENoCJ~)Qqd?;)$x׷7G$LOCvj߰E>tӃZC5tJqث\Ӥqd/I3:;$5DjcWp0W?8~UalWWK*N!*8OKښ|:' -M`T4|rYI+{$qfngg9 Ð}ldp6pĂ^PTJF_J%ESbGPx q?H 3(b E~>JJZHD73|E!-[͑[_")2}QQ,Q;$Nc ǣ}!3@P;ZE*<߿~oDxO=;nE7=Q򒃗$鉿hcJ?0y\G!zL96Qb)g?9Dd~Q\Q6l~zjdG=$_z^'߮Ў=zJmxZE4z+OG^ }jV&kA&L*:a ~gG~&Vov`2g o~PԮp1&o=E -bUMSV69.}I'?=}$}vCddZt?_bA%WEq٩mKCrcBdH>vԂ?d pC'I C/9I9 -`_8$B#2{Q';HzJP٦`b=Q$%E_C0q[g5fZoWFg.IJmCv2NX<^ OEdR&=^z$e+0 oU09RE(VH=+'L^IH":fnUp  hH [Ʉd17Amr\ 3u&SI֏G`crޞQ$5#g Gn#)ApXC )X"shA ,D!ц>T7۷d@acR$19$LN %X|EoD!Ɏz-:mh{\j:ȞQoG=$ zN7Ls3Tc&?si?(gy䩇WA.*) ŢA(@ I@Jeۯ_ojC<$i=$ψ3BG98R<Xt٧ "SaW+$NK%,(BiqqGi^XIzEW Hb2%~r?CqȄ%d ߮ĊT#v) a)b5;YO -*vg*V'+$񨎙ɉnI ` hr5 K"g"Р8=G췢ih7C7~

fqE9? h7?E"x>zĚOSGc+=Kbg)Ef`A%꒳$a#FyӳvjxC&C#=",bSՊ@p7?SЮF; CWIRPؽ+W"+29?+# EdzI94ц&d#$e0,D''Ih0$y%ILgqCSǛ?;ϑp]jݚ>%)nd1}5WBx6T&k]Ҫe{#''wCz${$I%1Aͧ6 W^j=zя)a<+VMu?A1r=N )a=vs9 qr ~nokFiHFi>uԃ?;-!nvڐ0& ,q}\z -ʢ3$)FEРH@IF"\2A xIr⻯ENĞ |$-wH9`ȿz1-j -~\H} -KN zD,NNr`cvHT.$OV/A,Ȅ1$Ԡ+䂪.V]yz64bGGrJٻ9*_RrcX GאLjĂUԣ Z ?bRWNnrӚC -cbaN/nOS{(jQJ* eWd1'XTz==^^&EbxYNkIm?HǶ,K+^I1hжzSlgm;LfEh_C($FrHbbD켞 cmwMz<>P?#nF;MOxC&TOPȏzyA&JL :Y Oωz4!釄~zp{{[z) K&On X~Lx&@͎P~JGJC%Uԛ( ?Cq04_Nrv$/;]ɦ-Ģ{$I}egQ>rRψ3B퐐U;$WSzsHRd -"Sjc8ꧡCbOA%E9ٙ?W?? -WP𓳧<5*3 cJ`+KN^rxsXC^3XE iK*~N/ގvl}+^EY-TR2  -jN^h;Q[߲3BW Sȉ d2,Ԃ,$릇D-=%)^>J04]jVW;C#DN0vs#C^v3JAiC&D7Aa {c#|ɡ٩KJn%YG6! b:A# &Iy9^v\qxMW,VzvHz iDOdy,8E8F IjEɑŏ$ !H``]I2JP+(فhK&MǒE\Fo>]Gd0v^V3 *fuL*?;?ĊpZiZ7 jn" VKJfq36Bcf^N6{͑XhbWN~~`9+],̫/13u٦.M/xVJsh6% 6RsښɓX[WCL!{07j&Wû_|^Ԋ8١Ñdy D)IjE!h@od'=68{"HDDnq%WO*SoؒILg䂨7Őltf%5< O9B Ǒ'(! M21BA3{mɨkFe7{zǻ0;#P\FL^OisC\a%< QH끸"KWoIr4v;*? ϟJ^.m{G",3((:7bQ]LfrĂl;#l۞򴶳2\mvF( -V*&4mnz\3IQX=,U{-RVBMH -Ys~,+&l>euԅ{ɩ.,çy4J]5KP(Nkd߂F0(G9lvFFg_"[YݹU.e6*M)(?=%ka+_?fJuONi9{rbѮ_ixcӳv|~ -~JC|UK-!Dpç!񩧣 $Kl?RbrerE* xBjR Ƣ39UzAAX1ZE0;5+o\fvXZ -*Ƶ`R s$Gbm#4>R#ـ±;,YЉ.GmH$sda2b\0??1z9߈TqXRC3d9)X%ӑb&lQMlRee,_]V#\O/F64 iᨗ|?*{q6!CZ(&IdY3Y~\~J FL5lTNyF]L f1ѿd.?q[M/Q߇z#"WEzQ=%HM\(z$7;vd6ɊC$A -Zz'7ĢA=bWCnygGCziA%fQ?=^vJGIN2nȽK qђ{է&m:9Ӣ]\W\KLEt…VS cXQ"5dbCD~psgO=$I"vĊFH crzÿp)'8u~wyޑEy$KdfEp&) GVt$afYy$I?MGEDZIΌ_KJp-WKH&;%{f* OazELR }РMzVX4[$UԆWO~~$ *#GErH[\WAX$>VψS>i~~~x󫡑^GC,|gK7JC9IRY.e9JA=Q^zVO qѨ~ra.{SIÒKl&;ّ͇c"£$k92+}*ythJavrRo f4; ި"5)⯡,rO9.;܍Β_Z rX3|Uf=爩&{#v^x+)#GG?OydI'.UqbɡM7U.(KT}Bq;[r׾F1n q<גߍj)&gBVQ~JxUfܬmHKgQt2x ^ۤ[$6 [yt8@eFJo'%5Bz⸗i{a *x'':x!, w~:Cgyy1I i~镤ix8@6թ$(f74MI) XY聊!9(gaa Ǐ4M O3$cy>%I$K$K AEC2$Eð>7a/-8,0<3-ɒ,E42 GCh((iz!zg8dIahz&&y^9^x~W~^eIHz# b>$sHP$$AP$AA ɐ,A/I Ep@Q3oERܿDCC2<3?,=sDTD1D>D=T?G/p Apq1GGeQΉ^)4INY'hz mSmJT-^WMN,WYsiT]ڨ$GSTz8!IbӓCώYLOBCzG?bt`IЁvdu/9-5P (`Z`\@Sf~*ӔF_Dϑ[hXH^2ETfl8^f)j!G=.VjԎZ|4&=YȽvnfre`Txg` NbXTe -go$9W1OAUl:$KlT]һ(8vW32 -7]v()0^т4;GO2>o3ԊDL XBc^3QFvægjDO+`OCL2-1XCl:(l`4)9)%(|z~<XQ IZtJvcCq+FQLz7uAٻ>`}n8ZIQosHq$E u̅5ZZWSe/\-9*,Ee_׽G;tHǕ_ɆJb -3Gj[X tX "A7"#rBe_D,Pf.Xj]Y󨯞}zbqepw; &CU#gIը٤KQINs&|ԆXLJ_wK5%/ޡr(kSݻ]K{ -:I$#JRvOOA fHO\r'zEn䲢QeD.3Unmt\Vף^^D&FٍZLѤƸl!N vVLc~XOV -#5Z -+\Y* -iAؠCWqE4|8"flBȀ)d+|O9.ee/+?=blȋ6cz_\.a 1[MaZgi>[[l!*Xw%EștJ8Z)8jRj$wƟ11u-D (Pd&+\EI&fmd1OlGUWC&84de' yae kn@ 8N(I\eV70|6(7P@piQd>̺NN+gkgc<]m/0埨l>.KEJ0d+NZ.Vn.CͭnU}.ânyWE̒Qmxih_=e?è,rp>;m`1LL4RVF̟.6O,2xlU4)"EH\U.դ6A}+x4yꇂ3hHgEX$#j{f&) a#FCEIoW#KN1%#W?(z-UrHѫl`it pd20@p\5VNty8!d "3"6{䰤),"|1d -rFrkh=   35׾âܩF̭o?=wwZGAH qKkޮhZҼv%(Lu ͤbOJ4aAkjvh"nDťhsANY;xOĊ)F$,'?՝9bdD@IAsXU8"$(-FX/NT0PV6Ӌ -\F 7bp&u؎ºeLRߎA= r(9m gYQv\P8^p7Xj))͇a- -K@7Kq+pXwꭡXj( V74SoA'D34QY5~Ͷ6Wk{ˢ}F2Vq˒KSB7=.p)UmWzQTΧH<~aBFmȷLrz% -WCl)lgoᤊZOA\#J-a}" ]zvpoAnJ2^KM'p7ZX4XV6F.G -Tm@a]` jv2]&;7B4r(YfG-HS]^[}z8.^~MvR3"542 ב`+vOD%UJ@1t$pi?;Nt"gra:A%ֻSYg6[`~ik=|KxgE$;eZ7K*2F4CMzBca3-A8ҬO$Jw݇K9d~Ra5*"Xtkn荑Kv?l69PA RXZ%()`b7^`.TX;Wr*JVQ͚Ζ䔠fD|e,1%frh GlFַYn׎HV\b>?ACvt o_`$0'=3!]z$\Z:^D9hvZG+JNmĴ~)?Wq;,%'*}e+#SX3aUv6CjD-}ݫφ*P̟#-ߧ3ŴEͧς)6ewlٟ p$CЊBLkhgEcrԎGL'uļPV7Lo]xۊHN,rCCue$xd٩ &%f]v]y_^*F|H(u6>~E 5iᬪFI-hr>gֻ3ӊ -d]vҐUٲ=!I oIm8$'d\z"Ί#I 7Bdĕ58XInW͗O2Z;\q$}E/&Jq WtA~8㖽f(,Mw[cd̘pbNE غUldg?Y ɕ`RZ CVϽ U/$ؙ?`rEyU9&O_ yB_WWTv[{+c+`z1ܬIN*2U0K>)?nle$n~X2 Ie55-[րQiAOAC덐Fc Y 5\K.cGA/;}(٨kNUQ- izEխAխ -\?О\yS娻׏楿rV}kh䂲!GJꅬ^%_Thoy$$lcY#>F12#vlH,X=#Ho8zYu'WD{kCe h+{-* ~ | 5x -./G?\E _wd*hLt$jUGE -`$l&;0H͵LELzvIbzp[T CEtu lXg)S*`t&_Ay̰uj.19T "w?T+%#Wק3TC!`ܶPͦXb;'l GC"TiUoA~3RΈTdC:` 6\yiIJ5wS$z@Q5֬d8tQ h@B`'D B(Rh"`)ffPp4gקW?.,lfcgɭub;E 8| B@)H ^ ?(TZ.cEOH%:SQnǜ`X l`&8A -XMp#58F D&D;`8KuvY'fcnj`HXBp!0A f -8 "GR`1CMz(4Jt vrC PC↌-N`B%e^0OCmqޖ:pXe;|AjTpIA¥ńsf*9X2=p&P!D&t [vЂ, T 4pĠQڊ͚)z 6C"v !?؁x*Ta -X!@ H',;A@X.j^rR-L;~^[D82p!?8B ! -RxB0%(E)` -S0vt &,Ѐ.R\4%X?\2GY*Ph@jD Jp EЄ/t! C\ 8< H'`TjZv%щǙ?j4y"' N0"Q!ta X@ +r0xVH`tì%(4YA5b F@%$ OhB H"Cx P <Ԉ-tB̊-vmhhW#r(Da ZB%(?h|+Da J0n00p vء,lZШYN& U"~Fe#Ŋz)A-Xa -; h7r0h A0 -mf#[&Ty'0=)A%(V(/>Î4d@1#b/MT~Б x` X&< -$$?` >P - L Sa#yA|*:I*JV,ZlJ0B-(A GxB.` -R`Bp!I hl`CL`pIL_Ú^ oSh!pt MP0+PA -H($< F N HA@p5Z̨V7ݺU msM u` @A1| W"4>pB!, -DP\`:h…fY`|͏UR͗ilb%% YP# aHBtACp`C4 l@Rb{x5MqBjaMŀf 0! -QB@#A1`V0 h -B Hrxq L  wY4wR| h0'( YMȐ*L?l#aR`xA.Cl5K2; M2+C :]Bz`ܠ#x|! T 9A -6@ nF -.BF.RZ9Đ9A&` -OP& I0Є$,B R HtP21&9- -hA2_!롆M. -c+ /NP*Eݫwmu|N~VR15†LX%Ѐ"` -Q)4!*|A`p 1fPP^%j_pKPfkT_lcE jHCxv @ D)5KB#f?C<z7WwZ5hP N@Bx A5(5*3˄fҹ|Sygo#:ukسt``7h:`!x@7|1KH1%,2/8/cD)t.$*燣 -%j@%p!`@4hh@aqb7Rasb -VBVʩF}ҩȗIXiE -< BA p `v@( <Ѐ La,`Aa)yJouK1%,-dnxA @PCh2a`#ؑ .fARB`)UfkS#6e!ra1BЅ,X -I0n "@xB쀁L -'6VkDNvnfBFq3xVI1 r~LŕGp lmHƣ01;.ܬqQbbI)%`ESgſk; SNI ~Lq7\Z#Vb涃 -dj-RX" ^q3joWHtfA8^e=FMMA\ɡdn(9P~00_Ҳm>K"j%o%oD jVMH5i(v*^ȬlWG]YZ+xfzw-䕣tC1; )\p(F_<[E m4~!:Ua'@TL-l&1lTE#0`PX;LQ%&GF/-RvU. W_z٨r5Bp, [E: $<jKp" XʶL?u ~i/.濽+eşg?瀱0ivp4mmWS1xrQqB.X%G  6rG/HcKH.MPKn;xJL*&;IqTZ  mGQ),eV:ˏUʫު.av9OzUU~ԭl}AUMqC0~>zWga  IC&XRGСH).[܏{ mq"EW|b_?3oCnCJ "NrP|]IΕdپ\w`={A8!EIZK[Iip'YVEunČ*+>%6Zn-֯+l65;8 &W`!hj]|U@!&6TCmSas+W4#ejp* ݋o?]rg< őXLlŏ^C0V^,R`jJJgL+oQʝj[`ZËF -L# -hIrWVa#nU0]Uݹ !> rwCEiZ6_[K}P AlDH1v+ TE 12(t*ՍhŌEb}^79^@.VZ(+ KMLED %+ ٘P^> ?]6IQ[t6zSrHn"h cςRv3:{#)-e6z K,ڡ-_sy~+/5b(*jqJZ6uY̆[ W6UVuφKi0R^+v*(* V @åvmƄۗ.:hqؕRH%T$'0XhZhjoaÿaʊ - -i0ƫ*dq%,r(+._;J;8-CĐaQ*!^EI4>YkJ - ) -֯<ώf',4--++-T0$Ƽ_6>G,,E 6 v*~ʬ Z8*0A2dON(|u=+ɿ<Gc;8<#op@n@Pkn4(p>f~X -ɄԢ\I NE$%y4U#S|]^ElD+EfB;䚜m"żhd 2#UCkf<RQC%hi阙8YН^XL*a6`!T pCF(CUlTj$ -  Wi00=4#HNU[Epc`7DK b8NJnEx9 ݊fesD6jglTb.Vd/P\'pҭgC4Vn:UQmPA퐉8aSoƫ=ؑX|%$r,@XQϴua/v''rz!lL(*,/1%ygU|֍}*t)h~ a&(T-lQa;^^;lRJp&]w)jnh@7NP `@H$eCE t`W+fS]khťW(YE"2[8bjzbjsЀx -idu2/?1,Kт!ŭE Jl?k@PĎ*2efLdWJK.cHŕ◞LJk?QEJbH)EEt.[3':T 0#FGzi,,gk#+:8`2 !\@Iء^rJJxW2Zt\fb  -0\Z)p=piP3E [+Ge<Ѐ!lLb4\5 jpAR\,NV5Zv"W)AYUhC%uce6ri9lOTmڧ'W902^T#WØٿ&;K3a`^Q4Yz'L\58U lOq؍^F:~3 a)942)r7Tl';Ù2bZZy4!34)G %7n$FA7@av%&/ɄuLK3Pi\&(ڏ]LGQ8\`*-7]s+8Y~ vq;Slr[^d= *.;F* OO/y,-Ev=]Zjf2PPsS]hRJ-]a .9S"Gq1s[Qb\)D1.Vb,vQL򮵗r*(u쓓|#uMe \Z+gy)[֧*,$5(UR^  TMMOa_\tI5b|тj]؋kqb MD`I +p:fh6Z^5VZ.v)WO%&P`pC`!EN46HWr-2h^rld`^9sIVf,k\5M Bۡy y8hl(=w&\O==nW;9J*-?[~hf.(9E;1 XVZ#-Y8 JK&Ƴ4XT'h:++KPt5b`nl(Anx_C,S[Qb#SI5LGOt  *+RoHE9NV(|c44H,+U%Xuږ%E~}٦LGMRr߂NX8 d_Z6JJU^mJ+gQcbSݪM0%6aV]2F 3x5S+O,vhc6TpGlCRɕ\tեκg?䠠,)z*'7e^XNI 7]Z GWBr[T2jΑ\U]`LgOU"d5)%UTJ)h#TK;Fq -סt}J>9]HhؐU21݅ҢUL /Q5ϙY8O?9ކmja56J8PT)h)$3 Ƌ 0IVY**ƫ%?|ri˺MRVW ]un䏇*,/thHIB7 lp6TP[x333~݊soMڮ2*XLqWRߊJkhV~rexU-KJ4_uÊk7rMnOGzk0lhW.#_b=.]Y).԰yt]}a/-ƅͪ V9iφ&xo5zҤqƐVPΒ%'}Ff.ݥtUGJYήܰ,kk8d"El2-zEmFjG)dݩ4[Fmg(6A>`xzW]4Vx*qmoawza[~Y5< ɭ~+EʱmnOC$71%a:[_jr&?BwR+f<~8Gq4靈-|;6; Ԋ^LWoybrw(%UPN -ŌIc)4TײKeifMV$qɩna.t+Z!'?CѬi;VJ&MRvU7_q^`aWSw-u#դ؄Co\dS(aZ7M5UojFc}ٍ@?&=d^%=R4տﶌnğF,0tyFQx;]U&7,# [uY#-ޭ֎39c0~fUtVC:Q~z&V4 .g+zKQ`E,W9%9 N0VMJ]v̏JrfRp>56ӊjQǥhWOe_ӊD.~TGtͻ4{Ni]ٙeY pf8~^{ORģ@~;q+/!x[4;ҍ;&-+Mśdu7*(gwMp~G/|maE,W3UCӨSiR(cuxүoYjU"Zƌ--ۆllP<%*[Gt0263n0DBNlX${M)1 -\f$5`Hfc HK򔒥^$V"B3CF#erH'iF z8"~q%]GK|E8%`T|v*^t))3I5YےlXF_fBA+9ѐNI$Y:^V&eo֌B|*UN*gc#wU4⒝}.w:uOQ.>2bm}}wqhYۙQ7*0~a/: dpW -x-W4ݟdz84kgKq X Rlk( 2?]궽Q` r.xk/Gɕu 0[ ffC p 6C'@ NvT5cb^;KP+s-+̷c&Vd\f"#6P! jP!Vh F79^f3o`p\N56@df ATz 8@` @'MSWKzp" n \H0pR, r@TD7XdC%F*SS6sUzFI -4;` thd|<`#萂@ p|@1*0؀_-7,E"sRJ%ʾ/Y%% hx:D `%\M7:Bjr(2VV -Y(!z:7]'X–L(%x# ?# -VC -C:Pa8ˍgSv}JB|T\ZTt5.0A H@)HE@ #L&$(b9`Pض/J~*86 -=ApP/YaԸ -`&P"xP<5:0!<ʹPjp*2h*V!Zp;@ p&HH@8v``(EدR{(zѾm^%ٱRЃX Z -  @% n ( V@ 0Uf2d[g>iz7h/)vT@ xAnA 8! -^ np(*XAnJG0VYRJ0pf0@C|,6jiQCa#Vs^~u邥:f_dz|X tC%0!vP$p! (`A>! P |c)BGcC#͸*캎ϷZ?ŪaCz$(@ p "P"4`->T GJFT[-k!mʭ.{r4w5pj !@GBd 8 D8p r`bʋ NhaZypm~b TmC d7x riBIx@UO7)$"E`E6dA& "Rj63n-l@xm1+m;a(?ώjRcc&@\Ё0! `tp@@-7x8dž⤩#Hv>B $h6}R -&bs$|+R,q=Ї<%VԡP쌔I8Jr4`"l:Ȩ(1'bJ ꖂ`'VVOd)۝OPFKXE\r0*Z&AR"pf@5;DLI #vJ۾ z0f*Jұc C*vcIn/ 6OtbԪzuvUi8|P5*dP^%%gÍ3ۍU,UTrh)G$)?$Yۊ*&0 81%+%MQ3JeN+VɦO/OzmN 4v\-vB\lٮ%FkET;aGsyY5KHb+rh Hhuqt_I|FȴJ|q'fx>HʤP6[J(n8I!CӍ`UyY%'βDJ!*APl~״nSY봚~ $ Aj8CHĞ퀤#2ASA"ԧ:_YwkuQӰ,**("P NIsגYR$"EK E7(I۝=SSrzgy-ȸJJyf㘅c.F$KoQx_^-۹Z$LZ|<+ݳS{nU4ըE}]Aϋ>Ĥh>_EbBjuW?чGl/J[XEYNbY+aĥ[ڟlXeQюW҆Lġ.Λ|WDE 4-^d1>R$GPw}ZvOQSg􄌠2$"sR\ffR -:J}ѭliO5:q5;ODb;}'$QQn0Tii|X$̅as$wra5 #qJHLls\;#bwFM;̾z 1!|淒S%~3zEW_|!&B&jHC]6U A3NSb(҈tiQQD " m; ; 7+?]J`'pQ|ͱԞ ge7>JH2r-`,)z)p#~EjB_5^/8[~H -ds"S}VzB2a7lxNrxlO(m5SJOsmzhHf,^pт1ը*{  Tϫuu/xŤq"E #}#$>&`~LקHW>mzHHuѓIJ}}۲gKJ̰H44kϚ,-jz}=Kd%?A+װQ5}ȑ$i?+J00^Y3_VEAQU]^u_fQ|TκY} ؔt&?u*j >E|?$=lz$\a'r-IgQX;+gi JklK+,|XH<ԏ$"!EFkh}~zvye\p2i~rrFC1hzWo$=x -wONZ1jmU3שivD2~FNoA^R^>NMs*[FA?Fm(]erJx"l~@jH=;*dNCԏǩIO<7"c!y$@|jT^ ,Ď "inOUҨ(.85gӇ<-s q7 -_$Iͦ굏$ReF>^SxV?l?:ר嶲U\s_EY\G%y>_H -YΖ)-OZ^SsIrԌds2vLYjtf5!IJ!5+!Ύ45W ?(v^ؤ1j23RMN5kzY q}iһϰ<cvYM:th}xW_yk׽5sŨeh&VQ)1č5RиF{Iv]ܖٔVMM(iF]|< m9WkM\~M7&YiD;05OSSpv{=ͧ OkڊKX$zJUʩN=p˂!RxdU#ݚӕ,՝+/%}DIդIIFI9jŕަ&Al|VMu(JRO׳/oLnVl^lǮ5l3bH>d(G|6nYeuDq]}sSja9[&6?%wtݫ{OѮɡzhOEo6_hTŊVr9zI)H{ٜ؄5Xj-,gq:q{\Tfm)>z#mh&A.5&4&)sԠjݲf*>+=ɪ} \gbFT2{Qât6eQ10|ʪn:u{Êz%Y\&$~4%/4,^k -W [  -_$95yP,_ءJF1^̑1xH(+(l9SWS.(G}82 5RXF6f[uԛ-^lHk+oaf>dC LˋEnĢW)\ .s3Ri~HDUiNkfQZ; $S;! -T-RZ:H!Ka)pWC(tNǜI3_vYI4ogx֨j -*ַ(Ld(Pp։VW]ST.UT(~u~?|wXذfdt2ҽ㓝HC 38~fYcەj*گ"iP:c6K1ڷ!ކKL(ٜ[jǽ*d'7?-$mIZ0R`FL,fr|p4'8( ?ԒQD˿Q8"z(gE|3H>_d.XhTL,>L/ySY2&7MVFIʶ [[x*DGj8@C knB,ܢ~cq@dQd%g@|@q` -܈A!e\wM 쬏mp ̯uHrⶢok>vL'n* - ƵP5 `#Ɔ -LwwWPkNv֋ -Xt$~> "x8ijnbGLM)uϠA# ,`OĆAc"j8톕m1C& >a0Q,RPC-3R^'d}E)(F<6OP6˗VV< OX;ɵTj_IvXar`x٘l0IL(1Wl .Ƅ+,Nl,y-|nLrnK(T~Lʢm䢰v5Q_N+pN*|z:a5YN\}l4rE /ᮎޏ|T仠g.A#Wp JPMb'_Ps E V`R}_WINa-1]fƯS %(D1Y伢7@p&=/J2D4OI~T^~VG+KHDdƳ3J^^L-PbKeHG4rG#7bl+ 8NXK_IsWHfn g#.TZ;U}#ū̝).-4T`))A;jt6l;ۢH~vXÍ}@:^l&?(*1Zf"@>p T.c0Zbdh}G;poٌ %Uz7GZĎ&$x@;Vi+v+5&BhB h v@BDX p@@ 0|1cĖc8;p.T3mJV;+B/nPw -GSY]۩VX9w(䄣l5,SmR3@qR[l" bX~&r=͎HBܧ>3E "v8*,{FrTxjTxXfXZ| +wy)kNJE*NZ'.޵U'ѢH!G=*OmD=%SI>Szblok&*7"*_kk[,_36޺"o&ȅ&0JR316L@^44A@4n͈حVb<q M k6^&`#&Eoa205AF~ȱ2SVJ٩t|pт _yDЂ$vq5  P,@Æ )fQչ 7ܧhe$8r#%*Ji0LT6SDRhSX&2i4RcfR`n*>J46VMey եGEPO Q8Х4Q^];<Fe# AJ$:aGV+wMP w[:~IOLOgf Z*PZH0LpS]3ŦJKI>tEO%# -WKR6(6bp)/_fy[TH ORTv]ɏ -QUK?C& wk:W^Kd'2SNuߐ)lYu+)4f-;=6lP`s%ȏ&C6LQ,JX0?Yp|!4|dէnGE'AF LSo%*;MЭJ3U!xW]ʴj9kߡh'v!*^fb} CƵmFWAzqcD<ŋ Vw8a)"pFuԙ;׻'wI8ćdgODX?"ǙOpW׭@‚ 6&+~܉Fˋ:PHu;#ܗξFapz&=> vTMY$iA3+/4΂-kG*18ۺU c0jJ+ō+,I9qC,J\ wK [Z(t=VTtp6VДdy'+ cm鱽mE#/\rءL-uzC\ry̍|{! JHovRbd?I&mP ]TQP)tɅJLFƤ*jJZ2nlzU'GwUJ'\9J)ꍆc8 -ZIG̭ƊL$7Oo>/6KL ,Tc4bCc₹fn&}q?&(A+b"gra٨%bnxZ\6*pB0ɎU.U`L^^Q'8E5F.dT%&EUQ/?GđfCg^9$H+(uU4)7 hk 2@Yfb<9K-Rrذq"N*3RV9ҋ/j ER*e M(9pq.6\rOE%xlHj PWt*U\% jV ]7 -m~&g2'?oXiP@L2&Gw|f@aF̬DT$?$3&kZ6v-!TPfN^J-ކ GMI1b"`cf(+v)&YnQn;Ltj.GYiu+y~p#T.p4D=! -ЙtZgOzҳŏ=zP3ڲSL^kVZ0W^D 81(å$Vzsɸt>s \hVEͶlV5%WD"i}UvlАAAq BJAѯzDA(~޵7`)QQE9$2tOƿ7VhISKVzaSKdR*ƉjnO4*DboXB %dɨkFYV oONR'l(IN *[y0Il?X, -W4ZJހ ̌2KN3,OYuX9=5\mi\^T, ID%PEV @6˕f)+7d`~mwO9rEZaElDJnXLOi'l>)U2Vfh?E$vcxT>lg{Qپ|u;-3dr(?.ը -gܩ|HNA0[Pn~)I ig/#Kӑʢgz^-K(pUSllg\ }d=#M2fx"ċTT`>d~?l_ExS ͥw۹ jxӒ~q(p(WUT}\ - U_p9 GΌdr7BҋHZZU6ʷIsbm - -Kɫu(FK,E+*yIЏW$C\n?CկGC<d(1OehVq$'9Ta80Aj̜(r+:dj4TZ1>]tP!eߪ'Ԑ#T㉉]rcI,X j~1?R8w_\zK790NV.kWQ\`n&?#="UELIn#+9!#ȝ0id^φv gh#04*2(~R;A1"KЙdhjnF!tHz%fnIBC';6R뤟W|bnߎE~?F!Ѻ]yA H# k&ozb;ƌ6h\P7,:$`=49؋WR7+ok?Kcxhɵl6bsK oya9BE EN_ 3DZvTT[~S ^#Y: ',( M_]r!$3==~PY.(\Tg?W$Y06(n>Y2*TfBРE WT lh-7^ka"$9bU⧧)y%upEy]U}beRY9bp<BF -,VI+5CKƋm -l|8E1?~ˢBdӎ#rL-/ƫ1-ƕ|T4A -XPxF1$5|o?o!(E)W"QtJ(1*Ou~BXC?uWGD? *ĕR|zG{բ'?C;pV|(/Q&9TX&Mo8;%UϯqtI\bl_#al0Yt(|.48-;-)/U'} aŋYYl7A7*jC]F@G؊O=ՌecfVTzH6hѩ|؝v%FLzkhD!1jG^_?Mv,JVZdŧ(qpLP-Tpb01g|kr=q"W_zȤT&Al~&{x~XOSV\PsHG"GZ=>,:I2n$W>*K*C|552=ًngF|ݨ”ܕ[? $#RM';)Vם٨+H?;jeL}sCːTRר練fbnԒX_E-lgB(g~jrԂy)!^~X-rf7[E)%%+ -R6\1';|, -K-觟, [ d-$}+J# Og[^/->%~ *+)+QIɭJṪS5ʳ`kO~$Cg -ڐNu%/嫎r" =rY85pu8OV$Bzc}yͼo2eU_E' bOۖ#+d#&e9ڲ=gGClGL(WG߇Ʒ.EU\-~ՓE 7IdF;lHyJA9ӎkiv8a&krՌ*P1pBySPy`ȮNJq^Ys~H6ѤvOAڔ*6}T4@Nˑ*b9#/dIh^чǻ(>ʑJQ=&damg$,2Q$6فإd`rlzvַYmP2RE0jטmGӌ -"1R=pl.K7b= ^ߊ -dEWfj&I/ה|\V8~N_lO=v'xYe/;=J&}TjE*Y4"-z-ی"qou#D~uf>Mث\P`QsC KFŦ\O/;)ۆiV2 OvRI1/%!ArG ϐӂ_L7]G+6uGH"#hç;SD'2]f?koE89R ncAf^.]Xj!FqlwcsVfF=E9Ŧ7+5vRtPF7~cM_G^5 ZVD2W(`7Arۺ͙8N}z7-OmYON#o˯ ,hUkzڢWמl$0DL/!(٬IMVW:$S,O]P&$G6{ڰ=aVOI>iv\Їnzy$#lb#8&'`^^$#zTD^w>Y}vwg_AY޲Yag˪hK- }ՌX>j1o?$+Hf,Ðz;Rfj JMʋŵeGVٷ͇_^}ϊRH%ÄŢA~f:ˢS$*a%)1*Lʨ(v4M?HSx w'7L_OPF[9!_ ~,9|loU4*)_l rzVLͧwS8zRZVfuoFr._xBƫ(aL]٪)&[bֳPToIqɳ؁EhQo~Dfw]iE#7D U+r+ga3H+WMrIjAX]ޮ7zeګ-/miϒ~5_-_O$_,oq|+!QFM,wO9z KPrTH.ĆcSԌt5&VA H[/J_H=vc%IJ~X -nZ=IңbWcկ@eR3&IgGA2(R#,D~*RI:̢~z4fX .j%gX'*׽}\6?qbUiXv - !ѺQoIU,VP)^6Ah _ 21,[ƥd?=)ǩ( -O)4-ۅm4JAyIJݪ,oa9OcMx75=OwOay[5DCzNv)~.G79~~`zUrC|$Qh[Βǰ[端:e j3.05),bW]rjj;bf/.\-le -JpwSwUF0^isaL%իpObgpiͮYVN_[RujIIn -ǐS|Bƿ~$ ]1,~\ ,դɨ-;K ?u+is'ƿ ȒI-dp09ݪ_M[7C<;N1VJh~<գ3t&-OYUgW |77zUsbܰa|`WCIu=Kb=! }$j_l1W=y*L/ŧ$XVlS%2M!$5nSoVޞvUq%EjM?D[я 7wYNvrPiG\ }3ǯ-'y75"^֧%Z$%HJkgMHI}4k/boi^IFaPۗ̂_Xp3WgʢM*gWw;4eŗr__6W=,#ѷӍ3Ѭ;5$WyE $%nf=]PMKI6ϑYR~ }0qT$JNwԍRZIi>Nu:s[rAr%ejWFL v`P M^5$#C7wIVOwO~S(mj(êḊS^^G1<Ť o>lrCKtV׿8Y)&wWSǹ -4}aŨ]@oDWgz&vCQmi1Kp"lv-fI)ې_վ՚$܎bQP6>stream -nVes;j44LWYr=Mz -ا'v/)YFΤb֣`X滿lTw*&'[>gþXpu,%l1}׏-*Q*UP qIȍ( -eqVX2_0U'5Gs2KIͲ]5h1rwRD厠P؎Cٵӭ"t`y0+%妜^(ƕ藾Gƅ QChYa+ڨEN #''=b,WAn[vNǗ 9^zD@_ wy,|uh>M竟NLJߎ,lΏSYSƻwdզJNw_+ -,r%Нh=;Lw둓}^㤥p5.*{v8%"7rev)=ynѥ~~t-[pFlPhR(KbC4e赡BE //ܳӌ"h+b&%(] rFrF[Z9 ,* s#oQ宍twr?ֿj=Q2"lwWzj? (Rbhߎd'e5`FӜ3*|sҽz7Xn]UPIE9A9nߊ.k^ jQ}u/ Ԑ5R\:;ǘ/L&MP n`f[)jY)8/81lp6TV13Fl˯-=ՍJLl_Yٱ=}+>Kn<Ҫ[I΍ڔ,r7rK[SLsj6TT WY?^Q4{",!%|ZQR',~Vn~H#ɟf{_9QlGǧޞvCzC־$f -ii@Љ`8Z`3ucˮrH$g[R6w;^h%0: s<ōdl]klv픠¬n -v'-d5)_k:$2]-†J eMپU,ϗ^E5DQW  ,-ΫKȣ L~MsxXy|Ƴ6S)0053DXp%,ە=DUcWrq9 JǙJna'Yl]r5*hvX JM$=H .+/w&z>plgE.q9}k~*ڌ=UTâYV5 -F$ -&W3)U}b淹m#Alws%.C%wq2/% EVbf9!bX_2jW2H#%MdV{>m#&棔Y+8G'+ h0.8`h H05$A}*\x참$OC)Y]5 ]h|Ֆli6='}K5gBEvR)ɊL Oi8ZrC,-2#(hh?=+zDAQpVrFSXӼe4|~~#vJTncr_=+TY(vB^]Rwn`v1}=i4:Keyʃ l*ƕnM0HjE}!cԀpiɊClE#;(pÉ"bN0O+0;lHkG91Gˆ -kwIU`e%WEm6Zq#H -Fpg_G܈ݙxſxm -Y0IpSnr5\A ' ^CYYUV9 淸q;3=^zbч~ZnRV,2S9fcfeOC?b8M~`i`x)ڄ4dvPLJJ' 5z𘘏ei.U923F3J&\ǩSWEwmzcES斒W3٧"㑢M\wG?5|QշUoE#xM+okfU\ +wnj )gg>KE1;*&%llwY/?$Ɏl~NX;ʪGh -GQ(֌Ws \ (A~r_T2Vd15[K .UR ikoEI E)nԲhSp)n%/&+]Fn^y]xOǑ)3AêQN1K+?d}VYKqja,FIEu|Y3糺M0ǡGP cH,'=MJR neHmț jv~Wi(t8Z\66Lݭ - |+waX5) OcPT^~VEC-FR)OV1RhEbkf} -Eunul20$x$φvئfq2r&͇U3ɲѓ\F  -B1i\8N_MG$4)R?~5,Np|#WC+*ֻ?H9hn8ht59TjD--=#c ubmNf(8:ͤTf/2^FBQ5ݝWf'68U׃q)9ƷCXeE]). noC8\M5+~'o]zn4^|٠Za`{.oE *ֻd_QHݙdV*!3hI 6I.1>"KC o?GMaZ`hPy<5`j6S΂tBGVggC&x%E泹1NT.|YLO]Jvڝiv6[RHtd uq'n$+/)l~F淬p%-1=_%+ڝX-T\5mBgn&z+:fj6)2=-eZSTtFUF*~{IbZE}*B-u2)7^P ^z^5*C3h6A.##fr#gC;8@ikݩo]F5r#׌[= #'n;^Q%EbfNjκH(r,5Eסfwvݖ׍QT8 WI)&h$HpA$fL1bhfILGa)WM(E'9`Pm }Ibj ,npg#MFlU0 0@ FV)l= ?54Ej(V^=F ޱrurdzC -5+7}QP)l6>6\*̈́feH6 -ZMxǝ $Ii1DJ Lbtɤ5J*Хjn(Խ3՝ؾSK٥*A&(EHE s'JM~iKKa'\C&\\ -\IGY7jmt+2cWz[^{Mmºg} drƻ7t&,OuszCoUH -I>Yͤ i&b 2z[Ξ4;ƋnWaL1NN4PH x2\J5o\P q/0NC3<;VEPrYVq*VqۃhaFI. -8qBǂ+,{ŅH;P@qY^ͭT#g,kKL2l@`/pRT?nPIHI()Lpjmr&(~6J#8IL+\(r+.u,\I)'WoX%BǪݑVHu&*v(C?,.9MvA(bMϡ\R䊐A3Xqr39\Z,l; -YgbltbRQ{"()(9jM!&P&.d(Kk&EP 5aSsK؆LFLJtzVq# -] l02};Uo_^ _I :dH2NR6RT;dRXv.u5CW+.݆K-BXcP>an(2,^.JR52^Jgb:.&%lMGK|JJO9'$F!C)lULH ]$QN ?Agd)k$)WZ/0Nxy56̠D`WMyሉ툑這F-4"q(^q|EC -Fo0bhf[Ϧjc^wً^`붉/rMoy5h.UR.v)IȄFcriɚoSSnq$erUC`z¢YH/JN,(V\PX8*UbboXA5@F,LXF20beôXUC6dpԽ+(b֕5_ōZC fzPF"wJY2\j1Xp/[p>ؠ(9M!`HlCKX/.;IioefbJJF1:_ݥ *W֭4(!#"{&V[tp7 n[AkڢIM^˫0ԃ[Ydd*dF]ȧN{vXqp1$%¢[Tn2jcnztbd{gF -Ewy$ջ2߆ayWRkf> -Of!2l:Vthf$0@xIa+ll啷H*Q#FƧZT ‚i2ܙ^-iNI6VZ h %o6JQ(U%I>[x#qB)sKA!Qr!VlprYsŅ!GMJIʞ0ڄS)EMFJfIɸQ+זWK&PYHY%%$-\OXwͭcbbh - *.Vrk=U؍d9ڲO gOC,p$WCT+rѥ*&SnxzCLsdPP*d|E7ˣ5͐K_ {qi)p'Rf`E ZIɦ (r+ ŎS`F - ]zU/EfC@#3Pf5ø6|+8r\\~ĊJ|ǰh +Oɥ^:d՟z1+F|+ɨyLQ6b\x6/m3YF: -iE?iVmoy4d&vfUQ/!N@ig~3Ű f@25N~`R3IN -O˗lcEC L -k*Wć+,Nf7) n+py 1C6Ȭvh6?EELrI)>I9+c#P*P%4H@ E#BѼ.>Bb 1F  !| ~5I߇{b~BϨ' /UDmM]Ӏ}dt)RZ[*ğ?Oޞ7^Nuziݡۏ"SNRmZ{w;*)QN~6.58&Ds%HDeȮy"O4W*QY'Ds%HDeȮy"O4W*QY'Ds%HDeȮy"O4W*QY'Ds%HDeȮy"O4W*QY'Ds%HDeȮy"O4W*QY'Ds%HDeȮy"O4W*QY'Ds%HDeȮy"O4W*QY'Ds%Hh~Gb. t4n}>VfEoXH$ǯ@$\'X`Sǽ@jߴ:OyTw]" sLrW- ֓ۺV6sLGiWe_Gl8ue3͹^hAPp}}T315XI<-04p`,"?%WYy`sX& -YWc#ʐmHA]B5 A1]#7 a9umFV&I%d4T<̑UD9[YO`kU",š{\C8^SP)9ۻ e$7ČºvN@E?z*WxQ3)BZ Z\8hӼ(0IhE-VD`U她F6@tsTMRV4W?![ G]`?6>=pXGAo̍2)7@?qؤp]G_Ź m'.H؟g(۰`o[xigԲ!>A _ {3%otM>R*HDj@:f/msKDy~y#LGA.zQ?˜Bu֯[l͓q)A+nYl\UD)юB^n3M3L>&zV<@Sp_} Q -c 6!N(|~x3YZ(.6N_h -YU@<l1+`Q*~\DxۆeŎ9"JO}hr_#D XF Aq)(b$K1f|9h?>5ΑS! H܄m'Ie&v4t*̆x8D;v܏o)or& <1q(o˷ &Z6]N8(7k*gnҔ!V'tƹ EFѥ@,SI@@iͱs9*P~#:FFsE(]>[TB0u)3 sGdMCJ [|?NQF&'7O,<ڷQk9oQOO^CSUΒQ+me1NVd=Y\h?YɗfA%&_[ɬ,4EXCŒ 5ʔlq~{8@Ցemie -<67J$jWH -Su_5hrm>ڦA( _))W:dLi^Ï gpY֓^-F9@qdf kmc.5Q95?I;EF)|(unqz`(4}=C*38] :$rx(A1IX M%<#޻#4ۂ1ENwr zs-ΧI;&1Fi=V #G:'[S%V f G-ZKof2sTZU.iWld_ D/ XQ4 DG᳜z6hலOJpPJ4ZߧuBy(< -Ex0ЏC!!h -pƸjUiHҸ ZM]NWfI85c{h:g vJ0խ=Pn>.O&hn22@*J 2hYrv(U/cK!ZŞ1,8M5j " 0HLXH;*&4N3pQm̡!mA %ii09DH2A~Φ] 2Nl+aRᘰϷ sNnC"H3:q-mاB /sʰ2LB&xqԯJ F4p33Dr9^74s)QV+Ty7mgKăyᔁ:[4l(ӐSpM[`mNET j]q_`X S'%{i&F&hKρ5q"]ZTÞU4N/ȹ9Y|?1 YCp-:}4?4J*;*++]DGQ22]!~99Aʒ -T&=0$2:)g|/=㳎3sm߾u1..d\׾v5т'/AP%MHNlޙ7<\ŖYg$jd!Jo>O,0EܔFiQgjUr(+M|6 -ٷ7JWN43x6ݟ*ڸUZU;p;RhJ4j% pi@N~x.lYoʳ^E?OK* - TfoG+Y,Sk.3p umyV^\! (bg`>8e-4yIqdטYT.1wFiqq܆FZ/ZE02hu`4;AҮ4G-Qy 2-DSj-Qfbd9 -k!RX{SG#6 :XXIEs1!kP8ko!mbv^=LR4cȰ*(*ފD"$KaT. S`duq!MF'N'NF5g"ǀPϦ voHm`hՕ@%ܳrHHB"~meo@!z(48fЅ~3HLx𡠉poi9J -h~Eo6NVYLkf|8zC3@l$]X$MA$6 U -ElmCDe{"Ƒ$`E( W/Ͽ7]˓<$ұGl;R\GK52PFY>nջAp̒ux K9<*6oxo/aK(]Oo{ȤD8 fX*X;gU . -W6稥\9 e@ Nv -% J$Ow"z[ai"@Nl`D}Y#'0Df;Si `Kx3@|:-n^38,xoW8,\x%4XETϝdkx,DK0_ zAѿ9{JfJӮF j5=~L1%b5}Gk\i[H[Rx aDdC)- 2o2t+5]PA@2CwIA\kR$#k,\bio"$ JP!rGVf{ oKxLJHW'BL泅M -UM)$7d!,̌\xZ+JVfb3Q3j\px6} 7wF}CXkLǤJfo(n9B{|o9oX ɞSA\J2AV|_QۓD0ӧm7ee<@I=2x cïM^@fi"&i>^|Ћ,]pj"~ߢ?S@ -^}hi&#7d )܎u%y3PeP^/@S< n9wS7`Ûl33t3PgJWZn*&,d%P`ҚT_L F/\8PL;"tFwzPb *ko1:O"15dg_=ī Lx*lŒ|]Aҫ#H!~$l>@|D VWe!?S7ZAGNڽfE e΍FCtnuk -R{" nr{ po˾Y {ت.Ȥ/S٢Tu@@ /p) 7"ӏEƑZ^1f*).` sjZ6djmHĨ<ۧio9Ykʋ׀ N^Cʌ;^w蝥y*FFjVVN˼>mС5QYi%Sq;Nͩm/0]Ԏ+bw+O2IэGO H k-#|VW%TRE -FQ fi#|BIkT^={Fbt{3d6j$d˒iL[XΤncqTĐO `ȍגD.v̘jR2m-[ -2@ -•hMؒ)(`5/yAJ蜥C.DI]tW^^5.zu<5Vv 5:ǹ;xS8MjTQ{|XCuTC5&?eKbxiZ՜3{9HPjNc&H#w XtUv[rl-&תci yNڄ3KѴk*Vh&8L'FFG-P:Gtޠ(jg6|{֚!>GR*#*= -PPr^PfqGk-.YM:1`%+23Yp` xchpNUVϐK}uચΪxь0=v5Q&@">Z$:!''_۷IU.Kȁ@6m*?fޭm| s(UP W,_} B<J~I+v)si)X -:c6WrWgj@uN!*pS">#DzSDJ*׈_g4: ͸o@[Z)kרk WCCd3"\u6 L̖(.e\'*K%mQn.^#"C4q2ia/l9;ͪvpAT"lʼP4MF9Tdzy"ʉy̐kKNynv vY&h$Zhls#{lI>/vy;lw2 5s@RaMV0864 QQXT2ޱ9n)*}]E@)"ĐrY~ŭTe :Av/%]łK,wi&xW;مlQ;#9ϗ5n4d*>AT"zT tBm -EP t@J5HEouR)`[`Nލ;n5Д.UˋjHjNM;臭VE=IFF8TH -(1/x=+Ҝ_,TDIkNPp;,Ft')_~W'Hzxh4DQ:n%8 H2W;OD4o4X ѾBk!-Py'H~MOv4]P1OQ A/UNzZnR>"R N5a]aPzO }DD+ Ê"2/,ё3O$ER4Q(Kr$[$i yh!:h -iE9[F3i!IC6 jDYxi 5QyߌvNC$x3C6ř8k3Vm l,4cEGZhBI(9ၮ8R#*U<ӏׂa75PɼP#(by&y] 'L(3pH ڤ}훩C|K&2Ͱvyqپ81[+?A9a~5CQb(nB妿lC-U jε`*ۨ|XBSOOIOݧڃOOϲtX8<\'q).RB~GSKZˌz9qp< ep^@Yͦrkjj-vCk1hf--ern6fSw^%RIImJrHN#R!TDi/͐}X C@9h"g44Ь}~"x o34%e⒢"j3\ _dZť&}dhI)Qp]don+%vs$6^\h!sJ\\`f8'ڬznvTl%`,[ ̌ۊJJIJID2ko'EeEE@aT*)0(0Zk)y)f[J j43YRsNNJPTZp._FSyQy!968Ңr"4[䖒23(ʭ6;Yq)/B4Z6+y LmBS9Qѭ2*tr4c86>r_n(K&-Ee%O@VNn' JIͬdN[r;Q)QUr%sBSSn*rrr!o.08\ɬ~GNF'Q-wpmғJNV\bTPn-~ zz|_XiSi[nbYDEEARj5VXj$SRR̈H$ "(T4@` PCA@ 4 1Hm93H6/(v'Q<-8:ʵd)&V|9 Z412 !6ad3X*M; 듢Xf4]\xfqfHW dyx԰s3)fpl0X7|pR+)KU0_v -'^MIEĐdmI6LzbQ.7+KH/^ރnEYpήlڀ eջpǣdHftW^F3?Yެ?3 B S=NkJ…;d.ϓ6.a -"$rd4Gp|Od Џ /eԛ.|DS<ٛ+DNq"M4ܓ }AV.w ܁f?eW-o{h9n8$:\b'8vL#="!7iGp)w!dw*;+sR TGـt -Fp9=Im]/q-678Ua0Xk(:tM -Z/b0=T6Gry$E #D۳_dbr4|a sCi1j<ٶvJEOU`]aP3=Iy5RM8OUq!'D=˨X0M >m⤦Gc4Mb+i@Y|pmb,xM(ˈh3!b- {\MtFDtf$Tj4S4/o.^11-lI!=)޸)H*bQ׶=)RlFaD( eb\.C*aȑZiO@iS-־ Db&꓅S8У.tBh! Sf1:b2ϩLEz&)).;UV$D 64TGjkC`ywg'7K/GrJ4)hAGm1ҶxE*_[؆ÂŲ'bR_ٲ8Ւ11Ņ勍_ƍu4*RGRסߞ WM)ÂF`RQg3fcV>~qJm!QS# qLca/H5RH('ِ,OHwd()sB!Zc_zшāyftlA1ǹ&Ҽxߖ6ɑR{#tx,A)\{HnGWVw}65f"k( -((r(UQx|GQ ;4 -ácCQH(;Pd R`f8(iaCEa!GZdQ? -p(nPlRy(W2a! yeJ ԬImaǍ 8DuqC+IfkHлMbI&ԽʴzXԴV9ɏ]DLy {*{]G -)YbP,O?n.uSbyg95r*K,DjLPl[25'i-3\59m5[!H$VfH2n)STS|Cn z˶]Q<2kC˫aClᅽTr|nl&@A8U wCGz<Ӑ{c0! -Gp(g'ݕU+i^=W4~z á0{wKZBdLAAc`&  $4# "P}JDqhXzwE}3^c'㮙*m oq>fǁN{.uJ*UoJ8ݱih~QڪWw6$b=:,Tm<QN&[C5I6I]<iWV49>v&eW|ڕM^vmnؐgy=/GT($J.?VaSzLaZ|,p(f/v{K5Gmn򧺃RQ{m:k3w &4їՔˆtPZVaAj|Jjv9#٦p gc1QK~ QL{qy,s9J51z,Y -d@@B 4Ѐ -8@$ @tC`p(F #ȫPn:myIj[ŐlʝcÂkؐ!D pkAPP~LԺ!gǂI:)nfD6A03ݝD:@udM_k%:j]ӮiTҦM)ܧY&.JK3 E9EhiڹT?Tu]= -Lg:WejCJ1t [g9X{4"ujWx+zMͅ? 1 [̈́Ź^qq]E#ٺV9~eM=ƩLWeZiK۲|DH9GܛY^^3l?naL $h \f@l2,˜14Y:v*r -N{!nVW(yg3 R МjCf,xF/9d32+Dtwu!Bn;Ja: -#+D]5?uǬ}hv![SϘ-~s9?DYdaQ`1 / -dT̻ :gE% ~%xhmȽEo< 0*Ǝ'tBgبS%XO3N9Q&tIܾ}D5#i*ޏ+3wWM6o0ĄFe2%xhMȞ) -@Wg#0 B ,ۻvQnX Ӳ匀0 A"hxTH@9Z>E#awخ4Gùt(? r~l7F(USI9sMF.ߏk8=j$a7\YߤL&9$iU}Dq5i ̰r|VHy*>[}@#=Po㡊,v"io(aEeQ9{q>pѥRH( -=<5: -Ly^ѨNPPžfB7o$3|$TRgo T0M+"Щ_蹛;AgH@ҠL{ ,^ Onp@e(VjKl6*)K% K?AkE7R|jas Vr:,\ 7EY!>W\DY0C_Pǩ BɀD@7V@(64!&+YSbJ ">';gv'dD0 S D  ! hݜj&jŔ.fɆDW} -.;#2`'"wu3e*52ڷUֲNc-!s m2^T~i &?ѿV(g&:0k29D6Q/@ {$]>b8bZy -2o՘Y%WTzHwO -E&fNF2kTUE_A.:qG߶niuNV*mK(Pvm-Q1X@ lj! +lMtL]Θq V$ 'v4R:u-ukU+Ժx9">(V/8Wǂ"M.Z!˴HtAzZa"Z+*Ras~Di8i̤Hh$e1 -k}xRg_+)֊rk]Z]=fxvCv6WP%fj/p_.|c˃ص6y rz[̙Z9$ae&/`@/(8 -O ) $<$jg9(kӹ'kŦI-Z&tZ筘Cx *k,"V"[ \s3Vmu` -Fc8ޝ'M>`A ްyЂ^ PQL*>$u<0nؚ B\D3s Ov(- N$d7 **Q}D&/G|"1#<sa+B*E`3<2~Āwޙ\-O IH0pVyi2,s@'&=^%᭭w宭<0mLfkA<~Nrp(xfTb̘5N;Lcn<Q&ٶHA+WΨW?j#̬sW̑M !̽1DL Է)¥2\`qb3I;OO9ޚlDx6EDï66 vi7pU{{#:Ly%a^)Ρ q_ٔ,5ݴšN#yC0'Gf t_b$t8'HXa$>IZ]\U٫Ij$:f=EBEEžsE\C |Z$HHTtժg -:HxCЇ_e7 6+o@F j#b1 -ʴ jv5RHL{#A" c \C@`$|M-f,ٔz9"% -m"yRO _R#'FWK4^4H*y"^8╸\ >Nj`cސ=1EYYy̓4/Wi_Dwú <Ţ˪EZcr+I >)!~ڈ sv<2ff* $gh3b2@(bW 1Xϲ"F0en.}J0B#sLC硷;B76JRt_B <,ګXgKQ fKx2bcAdnj4b0( n<`wZnm{gTz,'*.N%7_I1ϱx㵃H\ D1-iENqoF7.*M,;y_ͅ90%P늘aLzKP8*](Ap)di?URw%/Rl("osqڛ9 5yIJ#{ SQ#NKC.Ud{g }o: PDj 1L:Ŕ)^$SC%!')$רP +[#PiY7cy=VH H4A2θ+p0F ^ǬHA3JLkw:>dO?G6& >с|w̥G<ϼKKzr>/UjB"՚-yν`MSkK#VWԐ1ls90Oo5 27ݸu,B@ _+HÆQTGTPbk&L?`|A+$pԚ:p CSuKz%(Z'8v֘b0GI1ђf#(0%Oj"tjG&[5/3.\xA"678K~d,p$ a lbaWK -!S $E䭃*SwbEJv~@-V0ލmۏs+ќ*).UaQ!jjK312:YzŢIU߿ f -Z s<"N'#ulQ:؋x0] ~eO|Aa#eVD;`1r -:g@‘E>Ӟ2Dw.a(4,h -1V_kOcIa8@~1Hs4lN/^Z. ̋Zѓ"kv"w -a,88񂇞 Nd9HV'4Z<6Q,')w" hNq9B<*V_ a8Xq꿜u4IH3"x.'&2< @59sJrFm 44<1nZ0EAM_GDV -P{|VV$, hPP"zUyz=B -5ڹ Cqc-L.G$wD -^?ڢd$8CnT9Z(RFS ~Lo`QЗV0GC HHpPO֕\G\I7d')_ IICNV ?6 x 54Ja-r!֗שV?HcZ} Lu*'Nd[cnkXYYŠb>*k xr[1oHeqRJESif;D:έ Mļj5B"jDr&7[,Z{F9q2S(C7xs -X -j2q20}t2Ȅ-xMqcBT1tbV..bܴي'wZTLDjCP3ke 0R e_ql\fKOZ)9R:2\bqΤ #)4jklqXa}qp S=Udj>u֡φL!8O VC#elpjլf8KV}z#rVM jSU/(- ܪLjҞĪ\G`qV} -ט9(XRЫjɱ+7v9AXVT,*RՌ"ވ#nTKN+ᣮ%U^zK[1+jh]+zi^crkV!N&ұ (#m t(Q_lJ6^-SyG=)~%±ndL`Z6%`QUoCսჹm6.~,ذ2Զt#ե)6El-\|Ejyu$W$8 $bXmju1sv5O: X٪d:Ff(ˠSUj:x@Ud1ϸkOV}U3AwU i݄D֫3UK>[CތtAqvhjU -< EN M}ޔVM,ئz#32O2:e38ʎZCh^aqXyHjz15jyqw&Lٽ -ԅӔ:S\ ]XFuߥB#u =u2_"#K|ΌA*lORXw]߶ t?TzF*\92J=ЀUTmK#!0am#x0YmHdOr h МvIL(?>dDMJR=[#Ж'aۋj", 6hn4lΌ9pdC%Rc,5#I7nqDG#;Ԥ׋%ɞoE_ܚ0s+Jά3:qK̀pxapI;}鎖]JGAR[^3T>YFz#ˎl#8k%U| - {W4)Ck5 %S=e)QL(F dvjbE%j{R*oއl>AU=d_2yоB8u([U1 JSOF&fI#ďGDo%U&aТi!%Q%B{ ;ye4]Es0YUCkp[|bK6z,}Z,/׮,&'t!<ٷljY ( ?<,a?!f(FN'vsL~~r^K$W5̫n+b"U1. 9ir`&RCm)p;(eݰ6. e,-4c|EF׺zdY2 'hX$\ -Bng|CՉK"ɰj-Jx@X9ۺK̺kY"GԺnm'q_ |;rJ.>xDd&{/lxZK/'smjóQe %TAwPr 'q4+ |܍_a,^S\]>sznstd\N~xEBK VfI4fd<Q@Ԥ1ljΒ;Ǫ͊)Y*ذ2v[̺QE~86h%r99} JV3fĢb)̓D`H_s}Y]h]/3@,6 .:jEi9NU,5e{*yYEZe>[Dw.Puˀ"-R([rq2$h[v #Eb1X+N! t!28D73HGIMO(q @WYW_E]͔d0p)rՖ 6SIObaU) -y-ɒWmPB%,<1(tjsFo }"L k^D|r -\Ɓ1:K'^bb'֛<($-&抁T! ]Gt]ʗ`>>IWÊpN#֏gyXbSVd)چ3 x -AcC|㉌ѹ-#A7@]>辤Iqw hW>TVC> -sTG]u9 7]~%hܱ C,\(+12 9YJsȮsJ/sUG)ַZ+Yb4آޏߨ$zuIFmM0XoH?kgrz7^WԢnxl"C{RݜU:>޼@4!ZNsedt<ڋs:u$h1-<᠊&+:(ҟ{wXH1%B}HXq*a 0]T@0j yFcgwHH>vӕAZPH'"`ߑ;{yLǯE5 K\,P$w -tˀ9鿤(ɥ=+ e0۷hGKK>Zd(H~`/E'~?{<$`xۅ,8w n&`v!rYF(0d7"E ;ts JK ]}ZsޕQ~^Ar;4T,{%SjrsQ_vф3J{[!npCC,B]LS\Zb7J=M9ESi.VdV?*LOrLZf7eeIjР|@;#-5 I|Rlcy]ela  7і2g2ɉ$&_G'{Xyv@J`2t#ߑtm,ڶǠ~)9v1ԏr"h߉=aP7MPC.?ґ54YrTkr|u sJAMX)LY(rFa\.0 -ddè4}D3 +M>*նeG.V] 5r[3u?25tn1sI7jE `au!IAb)J"m0tlE@ꮯPr [C:groM\Aj䁫a?qb -Ömc{ NTV͖OCk @a)Ap'Y5Ӡ"cu LGwB!¬ %l44z.`l%W:Xu@~P8Mhp3HW#A8;"LG?҈ׄ&WbCZ}VK!461H4"i͇ yW[p>+FM,L=;F sedװkv1^lb.A9SҔ{_s@6k Kq$Mߔ'9ucS 3иju`&. poBE('%އ/''2fY~J:O<Ϭ|P9.2uu$òJu몺": c8pK͹8 =@qt L -SŐtTS}CIERrHqez>B{t -!]p')cqo$ JP1k$@ӑn rc#X ?p'V4֗Vꋧe#)<#xy&<B"ݸAt|6 *G0;gA=AA:m/zg&vڿ'c=R8$,_[., D]8~DZZXHr3&9ٍ>z~ܑBSvF[{r}O|1S2aV|>G6ԦXТmdZ4+,%!TKØNDjUZ&5Hv7qIoeH{8֤ F64#0N /օ~0*"Ve 1<\]-V(`EzOih9Fw[NC [vd5yZ?d8X */5jL TM` B$  4N^ u"&W7JyY#-BeݓVȓw[$τEz>>+$̯<|TVw#@'#y͸._8DIDr|N@Yx@ -E?RІX} ؙBKaBeg5KP f4T2A1A[paiX?Z%h 2lfSfME: -W"7NldI&?:ߏUeR4bE ˓*w!0@^bT:m2<) -,5,Msf7=z2*ɕѯh?k -Y*v!{WE}Mq$$+{' -ZY"+h^H -4@2hTtO`ݹ*=x;߈jlAD &;ߓ[ UG"v^sDAY)~ ѥ2eN!oq.\f`ʅf#=Hl]f9,Hs7B>3IMcHg(pSȡ.4z yW⺋NR}Ks?;H˦̭>Mv4Rhm# ei;1)`9J͊b=XSCO=w{߬or%mPdK ]iC. X9aG>vs@uȁ򺒩ʽ>0!uv*<=!۠`ɍp\HxoY+Sb@wSV:12k}ch&蝤U-|)˕Jʑu(h o\KL3c3 -Aꌰ=h -=3sH.Ok=VGdb$\ANMh V{:BS*<8@SyVRcDw08!ALpFFM]b@ET6" -JM8gHR7خEMDM"-*~/E 1qG{z˖;\< Xq<ŖIn6)*ۋ=H3~gu&K$Mɋᩓ9rF*pF$|▆[3.#(#{D`PK'uK(І9@|F1TpO]CG^Ds,CX!t04^ I?\kw+^ّ}k%IQKD(=UB -ۗiT;) -ZaxrgT6YΔZrx AP7h1- -1q|koEK/2uF괎SCλ"`QjP}!ttyr0O=N?l2DN)um E{p) vIߍm E&/Swq{[~m[xH_ݮSJy.ӕPͨ1(u{"w[MPsԉ##MY@bZ,v`B8@ϋ}"%I˒@lxwZP*<p$[j%fGd-򚑛Д6sEQȶTyGх,IM7D̽E}`%7CQs-=PBؐېYD̒"lܽ{Fq:aރL:vJ`+B+Ĉ#({ -+A k[t(mv͑SQ,G$c@L~TYZ^b LH$Hx0mJ^e˓#:4E^^jlESY0i ihnH3* ;O`hQۉe.NS7խ6 āҙ \LI,o8b8++灌\$H:Nd#kՊj -6\]- SPsO99.[h 66EE|$[w{t%4$! 6MpXhD:@!;Z mM[rwP tӮtTE1櫢fGz qI:<P7j.wʉ%( zWh(Dy+r -┡' Cd V` - i:ÿ:ig)#u˓k,ٟjI -NsNr!q e[\4CShANg~;YFn@v<}/cXd|ggcV 5PGRnWc`DbrP6J7%[hz@A܍dڈ>gk]#$ s#pzeq`Ӎ1G -~Am}c$ay }zpDxTY-c^\L"8 --z@=0۸G gEuJV}ӖCEDdos@w_d:S3 ~Tɤnt4<>S@yPY v\FVO@$va !cqLE -\nfDK1'Z~ߐ0*6G,_\b%0\Z%Z?7kJCRhXy[qXv5'Q84183ΰޣ;r$ll(Qbm|,a~|Ҭ8W4NFjW \< 硠9&E vESmw-Va -.IF -zDD*QA*J>ᡩY gG;p,.0 -BK`3e~w&zێ}$t{<"LZ?I뤺TĴ-QhѨ -4$nmxEQ;K%H"h'Y; ->{Alw"!I)r OږbK!#n -B?Jw EOH=ydJ Q0bX"AyXƛ#ƭMҕUHBŘce/4Oڄ#^9#c|oorrgö*WNya58rv[N4l2oH:h|iCY}$^xvO>Ԣ%h8O3zM\u@ErD!Q`IUBv<YlG&;")eh]IBeR<Ή}"ݘnToK7T3D%6(AO8C#G [$9-WM8':P]|ˊ-ОJA@U{g=1}3!z3>N"ۣۤ$;"Ⱦ1<WjXy@Qx?.MPsPʅғ*07O xO9ay8,?p+UfN#BʥuJLPd~ ɼޜtW ?.iZF^;7ӛĩ$ LٮXxᑥAifC}ʙ^H/,z-F 2{aO YaliȤ䫪I\,b {:7RP9ײe/ww*\,8!Í H0 ]HN8)&+T"k҂xo}2CW*0%)V$" -?ŷikU)Ha`@~QS2Eg4@ 7$8G+;X@7fj*tЌHd#'zQ͑-|*1YDtsjsڴ掙0&]Ry0#f̴")2ɈѻC9d5\hf6UU8@Ķ_QJ^d:>Y?q4a/)(Dwe.\[: z|#q%4|ii]3'!Z 8HyѱKi5 Y:rKӘ3J$^!;Q9qX,rFPTќ@4t*X9**b4x`#!x?0>Z4pfG$+AY&'zO1TpwDYETFH\ 1sB麁u-yR&@c;ԙA2ALrlƍe]󇞠'S~x%"qBkQ4+V1UeB5\]j'dd&AXmK0Ġ`up{ -èN&8<玓a狘McǺJyf J XP`t3ArQcEGuu%pk!vDn.KmV/*]^ZEr.beHi?\2Vd>NF"^*bNH 3\/|.AGjG?XkI6% JْgׇʗT6%9礆Q3N 3'v`-J^^amt @"=- 6eXY]JħACƊ(2ǵNGSʘbzkL-UJœR}gy" -ʄ]O+rN4ROg$画Oj$L`9&թTBMl: "-ˤ@fl9c#!%5#nڌ~f=AϊpA0fqj*F(~{8repJi*΀ң.(bI%lh?Z#̲%|αŽĴj0;w`Njk2"qQXW؊%hdBY9a˼ -$4Rkcš@@FV҆,B #I +U$v"嵸;O3kMc!6`K*AFS. ņ2݀edϹg,QȃSp7'fxr8Z fp®s,セ`OHHGԆ-PЀ{KD\KEt|'|U2Ō3X$ m~ YFN;xdb STaoV6N͙iGfM)hFQW&hU 9![AI1;(K"|Q;ʼn6zY!a9y fs଍s oLl ΜSy$vYoS]v;r\rS]0DaBnQ2Dy^nĝd1EBËwaN B- Yő%Ǣ/",j ⌀(}xTZ)jrH:lPUF=!\nڧy! -xLzT8e'G)s]Ƶ - Jh TZ.oF V)IRNC3Jݰua't?^rӆa^ .xhWƑp_q1n)M͝֎%,~H0ɏ4@9, CVQYISP("s3n<.>N"eJ͐#Uď0RԮ D#U\C}ñMK^S4E8]m%Mrt^=|)v ;S`4gxmi;\8PYAuZ%h:G =A؛_hU7d;A5kNi~tDxF'oE&Fuζ\ur$E5\)ŚE*!ף GwwTӱ%xEF>>몝R>U*Sn"QܱuZI> Q~vHi>SfsBn8kXr- OKX+":B^xH"Llp3 -nX[P]jXpX3Hxh2E&͚[UF[ާq2`@*{!'k5u~0d#Gi=-1WV9^3*G@E M|`ͩ A1^TO>q>lUW_=%"{@~*BUPqPM7Z_4PT, ق8PNn vh%rgq O>6A^]YiAh鬱i%=5=]2 Ϛ}ԔT,~ZGzins:Yݥś$}n%,djAyXiҔq&I9y)<1cQӝF.bzٷA|<58ړbgсgm1p=d+pM$\SX\KE)m]"bh0 RǪyuՊYǩoV,1M~SP/>7$o1O9jo+6N)ݐ.J!wG}Ls5`M:9>ן% " -zU4rBX=hn[ϩ[%WzH9:ĽXc; XQm8RzB~v -XM+uVXا;x`b&+𚞓pK#ͻ|ռ!V쯘s|S7r y閎'\ËF XS~4{j%H- 7&MKJ1Cbvƒ&bmp`qltUdAQB 69 ty^Ҟy0N -=Y CB*5XD_i`)܃Id'Σ - -M]by(\xS݆+xRG:=cFmí`J3N+W XɁe\ZJ^&=hAd]SgwF`"`YɭT-AO7}ùQgpOAJ]:|Ѡ>ɁHFG>8֖ioM̨> ^ZtUܜvvt;J1hPܓ@zbXɠѺo«] 5aP}>7Px B3M9RCFjbkPSŶӱy۝qT:RR -f'6$8߭%߉+t-}Fg<-]H3gƭ3g#z?0a;ÂXѽY{"wj( RZ/ .#H`ػN܃ ?(DD`ÿ|2T'"N񞜝9QSbf@ Syyp\՘n=3MbZTfbZi釓dXc,URl-1fLi}[/oöԷŴ) W5:bzQdK_۶kYb[Mݵ约uTp;ܡl+KMk㸵5V*2'24sʪkkdd[9tuUh,m,KoKf5f~LquMϔEQlL}5#e^,cV}7 47e8FWZޜSTJ?[qRe;ZM߽ ~tD0g#tݕ^f//e隚f/sv/o@(5-uLs*ޠ` hoY]edjQ (xOUP#,GKop^~a@lqyW7ZCi-n~Sm^MEsckgkGsUeJKK;y#ei4ſC7?P.{ѳIJk;E4yI_ w,{?7I7ý"ΪMJvƢ& +hs*86W -fQu^i˪*+-}s;6E4E0y䖏i»w\PQw]c^t^n^۾A0ucSל-FGFAYL7?}Ý֚H-fWvdiǀ0iL -Ejf]Lr*rzrE,Ki4LNHW5ê:r䅫!9G(!6xEkOq9݌me*Zp2461VS̪؝# ƲJCCY1 ct{+7IqIqe)\ɐ$3YnzNhf,G/N$/=r3RdI~MOr#'rGQMs ɿ;'Y^<#(p{d'{G9/K/&/.9?r)fn￷(C9 7,?ؿGwX`6MsC2$7X$9X$}'G4H;{~4m4%(?8#^neI7h#_"{A/p̤Xb)Nރfap='/~f>""8 |ﲸy{q@ȕy\ K_[.w'Ew,=9 "m 9h~`_9?fo/;HqE/~,E2="E2{${]$("I#??)d.`MN=f=cM,Gp@8Ǟoq"YQ"(7 KqɑhaEL?\@8?\fG`.'{XYͱ99N2wq8IIER,(KPsIeY,җ=̾dI0o҇Q 4'Ar$}9 Hr s8]9.7RL}55V]Q(zɱ2ss9-#;Mr_?d;,.ľ ;Eqb9h"O9(@$GS4Ms9h} 7ͽЛ}oޗ{b!W&@[)ā [t -X$I<ĝ85j`f`L!N7, da }\@WK#6 -jjWjcFAK1Xcv@[7DtUI.<-Ύj1Å|i!,$}c!K-Ec㶬ۙ)d(@ؓBr@ -$Q-xPP#&\xq>ķi mJ -u 1FIIǒ`AHDJ #H ,"QDLC"@Dũ@Oj AQQ)>z RD9D p 4"'yzy:TqaQB-"g#4-Jra3@@L]la8aD%7 pʑ[h\AB΁ -^* -ApJŨKZK4D֧rZqe -,V/ &$PY c,ML0U:L9i$c_-~o@HpfK-Mw +Oe1hs /ȑG8pЃb0N/#!ԓ4LG%XiO0@;1`>R6Qzdqq2]Ut8D +1{,e0T-I.KW ހ0`hp`,ɬT),XHIhi!Z< Aހ֢Akp j O9,txkjg)E P)~.9nӆ wI.C Rΰ`2;UF+@O1 x|ٺZkT0 JgA5 'X=1Zkjh}iJZ7*aB BkMʊ@Ak]Ph]O*+DM @: HhVچФ,hIzIk߃֚sH/kKo@(tZ / p?Dƶ[,PF5cA)mD!@4%zhbnDHVeFkZYz%s*^U-9:SW4zT2@ OWW( l1왹ª뛺6,]V c)OWVQij̦3ǽ-ⷒkL˶ Q,Jpk_e['@_ec蛲*}5:c$SvĪ1^z']sW}epz! ű 6.(T8 &mUͬc56γ4e𶹡fVEk5VY6]+ 30Ue3YNmqN_4pj;)ji USt Eߓ-5tԞf,Za^bV}G)9=2眊ñk)jmEY@k3Ǒ! -"ݙ2eEAx\Nij5=n0*Fej)MekT9VU3V1@m%Fe.MgkKL54*fJe!| d?p9]Ljy'Ir+9Qd%@tsiv,(xWɜ"CV6CPugZ^w9on6ci-2so2yMN%޹N۷],.gpZlr* pUAƬ1 -jua}y6#vMhkǩiScrp11]\ S]sm dh"0@cn<8NMC ̠xy9ml92ܴvոi%6]iHƪl21Њ|ird%F7kjRdC(n]S*n;i X]rlj0+R&*[SH<Øi2[sYBJQ& ۗˏ -Y9($ʤxǀ\ uNBW&h- |&d~#xo@* nY",znD[w)\ a1l8{Ol~S9Bb f30n*ހp&b%f^9>2z1!ظD"k#OfQhS7t^(fkhMr^REJK)SH} XrPb`'a]w2,,=:aiCˉ rG0x@x? -lh\Һ3f -d8ͯZO:"7 (:h5tH-M5IZhӥԑWp@sb0`%pP*V˜-:qIS&*'|3"3"B`Vx`:k3 #%bql/S^ q>]%9TCж$ v؋ -YO_;[Wᡖqo@c<*x;EX}"of`An$K_Z1%b%0D]们}툥& o@HOs*˸y%1[O-κPt*nSMoko늆Y5l699M"#eόEiX{{W[ (WWYtvSxuY g,jm82E++k5 ̱t vcdi̪aN0ZKvC2 뺯뒞,eUZzQ4y7MCXYnpX{s5[[,|]]paqUWZ8{mjZC8w0ڛb5[ tM[h-{\ʪ+-ỲuB q^,n1a涸MC[@Ndo-thi˜@, h9&s c9sY+U/7 H`Ҽ!7 -)ozP΀K`:k]4r ]IMCFCƲx&LEd+3 PrKn$7GV.)lErtFJ" P U@\]zj[ Պ=B7 –nIEz'K" $jSiGD04e/hbeE`JT'RZQށW hʀ7 &bAR O EPOeH-D$09U'w!rb%ƄBSʕ&2U"o~R#?<)"LIZhǚpFG F'j -]$ -E@.2Pfl5 i*:-|lF%ax"[*>zyL4-yR..)<4el'S3NWc8Ź-sjpj+#`l ]6ܛw4 xZp45p,eP41mH $2CT4 Y0J F ^*% E Nu41ىc)OⲼXr;iUhJm5TWp-܌=dЈuɥA'+ސd;AppUW $%0|V*:";Do@آX>7O9-߱#Yet5!CWWL+_"{Ey=5ڰ=ywEqF9ܶ0Ec> -q ߓb{RKm( 'g@-PQ(v59~@s:("k;0+R tN5ܠ˰T\FNor1uRe㰜w9E6gDqg=}me_y r|2Ej8.:[ -pLV ,pk@5(P~XK) %h Js&P -wrnnSٹiB M]xq= G&xux':NN!>\G8Nn8?͹Tyө,F0%+O _Xqa 1.pqYJ(!aAKA\.Ż>E*Kgε΅E'6R& VJ#ݻZN)MP9[2~`L`%v/ `>zADKCwB٧)7[Cn^W7 ۑhfVivʎ*+9Fu]]PHMg3'0OGx6[!`+ժxx*\ޗ++P]b8mN]+wV, ?=C,baB]Sצ4m -u>?3NQ'4I1%&Վstt{&=&;ʨ3~Nu:|I')xdwlp' po@%qIO O}S%KjXFiHLf֙ĒOL8dNU@1Z#2m -\X6Vpqa+daj_ -Bo@`0 -Q/ -pr$R4|{^Rh Ap2@C)6][uP+D!^fq c]k!JcT --k#$N|z4{5nQЧ٫MTC. !~%?8ɧM7[CB&#E -R0PZ:V?%XD>[(8VI?, @zA<{ؔFE -Jtj32x%v`K9r=DEA`@>6)==׀@LPSTgISf;fj%Zc>H$|(w 0PaE,: LJZ}wc'(X]aSŽ0/ $:qXSZ̎PCpYSՒM-Ǣk1W yPܗC8 ((V88_=YRC04 &͇(.F78t]I A0XceJ"!$.I#Yz -qG˃Eրvļ]Rހ:1ԣi2u  6&&+ 4Q' z%Bm%3"AU%N0-!aD\* `mK8i օ@mB}maS}Xwڴ-9 nA 9B%(mh_YeoG-z^a`%F!It!Xlac/{ulcEPɩZހ/MQνn%zBfW7NjHFx&DqAJ:nDAySדښd3DJ (yB/ 77 Bq/ GFzJPqIlw-maD fp, t蹠B## cXң{`8V4>"ıX 8!bdQH!8Aw6FHJI`RBg> 5Z>9@(vyK/$x :(*×K3 \Qvǁ !;p+{TQ3\P%ߦ08*aMg` -w%rp 8y13I|w gZB\h {N3O>]kw*r#Hn˲xH - Q]t [R8'Jn[)$tO""Q6sXWCE\a;Wt=-mKHފek%8EilBk -Bҡ]ܰDqIzJ;5H#0ocMDԸy:`4DelClT!Ƃ<uOD6ª!ƽ<&vf E#`GfX"9ė /` r0H% PޏMM6 ?Q䞈 -WQ)$CLvj6!Vܞa_B#ifbo¡?!VH%X؅\Zm!,f ٖx@˜W)NMYL.5L&R ))~/U0*8L'Pk1Ѯ2JSn EUB/a_(r%;Z|j;HiTY1+2+єEPMˆZ!2i~Ɉu`wjV`ٺV3FM625m"XS6;3,. DfȚֲN5|KqԧP79VT0R܋xPu8j2R|΄.OFe7\i⚷=:*J֑S([.A,;p[ ڒ -f_Ђ1xO3'h\ϩ~njy%#)x$V  -QOEl5HR*wH-H*, 0!iAmta09 K@K|~giGE NSx rjl`5cvC&^x3|V8zq,(+֟yӁOX^_(B ^ Jm,O Yx&Ccnc -w@~oC mvM>_&Tw{LQ@$e"KLSuUKE%k,r^ [(((Po2nH -$wwͬLc:H}|gS2` -S*La1`PƼ.#%xx9ߴC + -d Zh%m ұza#nuO ;Aеn(>̙Ǣo S*jZGpq=Gb-θQ TwTbQ +:1c|<U(cv 9*~DS<iG 3,GL -(bj\$NRw604FSn8cx͋- -཰r`VLgBM| -+NF+ QF -AU(ƒs~ Se,UtG-IK`rew>8Ց#gD"y9*n@7 o* -YgI2`&VANeMӷ]RYt\5˿`Oii;.ꩯh *B8n$>ƨ98:h(^7WI.Ҋ(j]p+wâ!dH*,nW$opQ݂E6Ww~JϹ… EoU\6ٔ͑9e^/?KJK!-B ӁiD0ޚ.cA]P6-o^hHwL3t\H u4Ỳ,Q?sk 4RǓ#!Csofm0t=sx99B츨t& qg|Skw~s5y;s6,~Y{V^t^(6~/΀'өYC]iW2@ohi`;!Lخndp9 ѱdPRTٜn݅c[:hL]Ƣ(va٘~) =wnMb)=|1R|)q%D9Giz/ 4S ]J' B^![!(!/ԛU$ŠtP/4+4qէp 4/tQ6nmFP3A#-U"xWPSj9 QPR|26cy!D׉xru2--|7Nʒ\ˑc"@x-qE'[~(d䶦6pʖ6d,7*R#8N1±-A&%CKXfkԛ䣊 ݱ<=.y/DvbpXhW?$>f~ Ml3瞷PXg'lQ>y4LZY$37%ۑr8[2m_/~H"&e7tÚF h/ NL-Ŋ &'PFi# tz5K8U]84cX/%9'<#0Oܮ?Yw]v!}ˮ<(ü2NVQ,U2"40hՋLQț5/ke44qyXǨ#+Ιb=|٣[ 18V 4͍m{DGi S)yY:tt}VtǸRpy3Թ[2PbV5"AC4,//^s˟6Ĕ>TOaf҆X8$ToFIT4PMkf1Es(ކ#k'vZBy'/@r~5dB猢> pQ&fvXCy}ݤrn` 8 A$ff;Bu,E!{~:8͖s%BN>{1u+)7{hg!dN7@>Δ2h^½u0,I -(^c?>3h3S`,JYa >u~m9w"2W'qo6 Ɂe.[HbVFl ^:" ́64~'޶Ugީ -\_ n~1Ӕ^CV 'P.@.Z.ck>cm -! 7(t,A)nþdBrd8S<s/6[̱ڋA3M2iy2ST!HmoDA%p,ɭ p Py1bP `C<6kzFzQ {9(Z=ߡ:8b8p{8m!%')bF?H^6Op|Afe͙t fSgU %`iT$Yg;d y?7!Ds[&ֲlot֞T?TSv `<,r WasQ\j G榝w[s_Bc#8ƐmYc hx|W6I7_YAŨ6@`p?|NwȜ:sCoTv1魹ޞ Ej,qg"Z 麫 -Y$CG -\[>C88؛*1ˠwcgM-RhQaĄ7xfw-RPR@6utMQy ہ[QDխ6sh u3@ :lB^$iT'ȿ@\F0ޙ-0Lkp`T+  yf#270ODdY$^ͨ$ɥ-UmVպ͝9h]6GΡ/b9onE~eK ` ^jU^SwoeERu5:g2q%SLd --j)BE%5 ˿AμϵO%x([.GKjɼq:Ò3iΈԮ-ڬ,h7pVOȒY$lzszϫ,A&뉮q 4б7ʷKс%d}lO*/+9<%ʩl !J料 \||x7Id}{‡NB]5NȈqZی9@cʍT2r'@y.T eÉ"ҁJ9H+Swm3[ASwV="[}Z&5a]6-)Y|R D tuWRWM룓S֙r:_g#ʲ\IEc6ey!@>]!!i.r"j:4'' oƋnA9E{#N"DђѡJmZ;RqEcB4`p -i h.sÂs'Z(kIm&܍PԐZ-*!aΆ&"u#X6ļrԗ)R,eE .cJh@YnMlM"Xkq]pak}'<$%*U,/&F $'AN,)BIl$ -!>txOAhfN INUCvD7O^>pO{f4@ ߺHvQ*6Y Uj*}[]]?eFK:I볭M&JV!l͍{ޢClp@f5 C7VsJ -5  ԕK }x[>TJs|q}#n*dye -tt a' Al ~qbmy>w U2[HMMpq9u[ǣ}KxI*ѩ3>if])a_0>iQw~4-06n 6]hNY-;D%f= -$j3ԏk<+D.}%aqkЂDn5pHlQ/b^<8#,${gJAq~<-!Q6>M!> hзj $&t,?cz\v7Zj;{n[,F Wv> -$$:q!> 3AhQֈzzSJ Z擩,L8l8ﴸu@=Mt?,+3FcZ\]!HX Yt2X";yf3uCl&uT t p>~:qEf sȻZUбF٤?Rg-jbP,Vzq *2O&"[( Z%|HX۩kxU@>hLN梎co~`s%xr\ULuts+q%3)*Kg2V5dQ@({CGEGp7 e#Jy3}Uh͎:蘯;(~lɑ\Oev"_ RO au%ehsB'R{NtBd`9Z弘l70*=&' %lS^ߔ_b 5:[j {+WmDW]笔 -gA32 xbYwN̫6^;ufk*uf R)VκԒM /ZeVo )P]Q8HwMqRk&Oۧ- S0$>\DhGjdHvA^)~*M)xR>2ɕь5'6.'yP=2'xYA~x^aXɹRّ~DZVt0/T2Fwp^ri{k渡XN6$ХUn6 J+r9v e=W!^`׫)dM>*9z1?0'aj?A;>cb6 7wEL@o(Ԍ!9זNƮ_ ! PCē/[2"M?E MY8\L˳ź̬//rStRew_ou f{S(x F/R.#T,P"0U⇳UZ^HMbEKL^:!?a1r0V]`q bl] vyvb&BgmqEc8g9I)MHi -hˍҗSqpJ<38SݙhqD>j nnmY0aY94~ԐUOKS}'SIV  -۳Fiqۘ:Cgn)::ϭH/qڇr`$*(:ǘk1rO^Og0z)jj?`#fŢL IHSyqPna4yAHMm.ɗ~]TvrtPt睫 &iQFcӠg3z) q4*ӫq%鲿i{Q}%Z~Yti{y qh*/囮`qVh/Q: 4 Y Ҽi7(&""XAO6MA.zh ftEѳ>>=. %e/W'C*)glL 6ȼ* H*5`hO0}Wԭo7JOǩ~K\Е!EO ʛU(]<ۥ"Op) LK_N$~_n򟋣x8ޕ~dqON |nqL֦6=>Qs3!ϷQ=5n0 2C(!nx?ovb …&jcg3\wb ޕ1/86o4Vuur3[AE$]G[`xu M)"Lle.ruQh-K/. px&Te-:T"^"fY착ǩ.4Vs㍶ ZSbE/oXԚ&Ik>m?kxPa7錕 3Y"J!|NH5)FnzDzgD.MV#)I -:jkQ -&22JRH<.[̙6~('ڍMҶdP[5 XfΡ5I]7MYn3ݍ|zsRJn)WX=jԭąsnv26+}}Q: -C.5; nrԊ:h#M,I#էQGdrf<I P싗[KDsTs ]4(#@g`WwF/)&u$GFcY67Sz8볢9hEt\LeoH$ =ߩ3p^aE. Қa 6F` -1Ph({x`|RQ^'QL!c%]eݡCAaur4x6K;OL5e//^5N0 .QPJ5} nA #x]%v<W_2/qB%VdaM[~DWjV(&(kĝ5R5q &%$_k| J,IoYS_B"~ڧ??4ju遆P=ÒzW-FOV~X3*HLʀ ʍ%h{b?V UL]<2ZOQr9ЭN#^F1 -&>y@*aE뛟 Y&y(Bi=ILDצ!Q@eUDtXW6-O붘ݕ>G8; qDE#bhaF̽#I t%<\]*1^N|) E\d6 pЙz.f֌Yec ^FQ0-2O]mR!ijĩ~;N!7Փ1Xѫ6:;A#նD­B'roиra - -_VP]0 LtlBR6E/dYi~1V:7/2mldm>u'̫dlG)+A^1_eZ(]K^۸.+b:tGk`,uv]a|фF!~1ÂWqIx.i1Q[ -Ǽ$=iC0◲v(TQ5uGtva{'IJYJ6nU= |wcõVIWD<&g/U쟜@ȎM9We?,)q2m$Ad{YKa߉`B;`#a&_->ι*ʻ\7GFq-_Y2άdžC*>|¾Vn K ~@ :bC5#NX:%Qkl7Sp7nc(:6ہ֟ oCVO@TkPӦ\ - hts zUb*+ƈ),3Knt-e *lYDl!: gs >1 Rl㒟4唘=]yDb*lf0ag.?5Z LT_U~@H(SG˙#1 aQɧ,d -Gc_ -r4fcC1NiDy9>s-)Er[<: r'&z+cd.1vQ%mr_<Ŷ衠W;?jm,3D.q^$F,+ϭ;?KsdvOr{g~_,O6d#P?E`eόDjxUatQJdY -M<#4! -endstream endobj 15 0 obj <>stream -19/&wG/!۾فe*m::*X"g Z 3C^6ao܄?nÍݚr[2eG|IAOc{ChP3;IuDJo>Lg5~I Vˏt,kp y'P"1 9מҒj6?žP - (7?XJAP#"niqcȷuQT7FbTD.KFy ÿ -/(>k`!qR|'͝G - / -3B&z\Z9"s@BqفKیE:{FS D5RJ(p!2NY%?lwJB|BEЕ!sH:þ֊&J_ٴ+We$V)f)ՁS'n[V -="W%`ƢŮ=%MwRFvwF=GFSS ]@L(NKy\/kYL}+V/9fod;;gp+9R_([  -Z,!BK@9jx$C"M0νehim2 s]̧Άyh~K@yT6`IDyUrXb^R^@ Qeâ*K -gώ%ҩ|mL&X&LFL%ra) M`3,m=BIi,`WZ}SfHt*,zXE,e931L\Q9iC+{8ai_:kD4>G2fpAykL2m ȭV<` L/ˁ8Odɭ$έ0QJTOᏖg5~~wCS-`zԊ΁P9ËWZ6醭3G #*"cZ*⇏`,81iGlaC"iq¤ZKP,#]pc*"p[3"q[ С-RDkiN|;TTxZl'f,ly -ѶK -ͨ"RYTp6P-i~OIdt#Y(lHOuF@Jd.3WqC J,G?>"Ū)Hp):\ϒNA$=A]86Ú`ֆ KfxUtm(f-'\Oˊ lb44Wd"0o=# K #0ǐ 'j}ꌄ4-X -._.VA+!jv¨TKq#"d!:W} ix+dK S5E@T'dLH8!{yo˜8C:IM7]{LL`*u -r l{z'=^]&̨оϜ+vF%mPvd]xfiX%WI[_`%"栓f~>!0{"roY$D^afI!C_IuaDWM"oA L~J3J)i9i%0B._60h[;l.@К\w-8lek]ntFHrot}M0^߶%1Ok4~]'?x: -~FHKn"ڲR-/{a^# ]+'^C']s@F|XxD4P w~k''L*mHsBAVpPԿaHDR!9O`!FD - m᳉A)PQ{Ju>`-D]W}}{w:TnvHߥg\|~(v̰͌| -cscdBA6!r1g)nJ3aʈeciڰJlX<~DPoE{ŠJI:Hz9Q;Tf͋3EAtP ྒv#r1%u=5CXsF/ A@EN>T߂eǩrr7;U6/蔢ŷfsUЃ.Ƙ&5&ÙM<-Wٺ*ܢiu7vDţJixڌTQ=  -&5rGؾ zurŁC.Vr?}u1餓Qq-rnSԀ# Ͻ)}x)Zw(#B|tH5( i+v Z)M_W\km2 !ї%wd0A - fW+P"͌6fi;g'6~S:S|& ɸq[v'g.^71něr)id.AI \z#CF@ù=.}ئ>XJ.@ǦVGۉ%:d>).6CVJvK5;Xհ pJSgm(),KNȼ_r3{c?ܵmL23yp-tc>Q$с.x5M,bs7J~S^r39I  Pʉj7=U!#'~i7_l?2mӘ54_` MVi -G%9k j>#pOW55= ,=JvTLE V##ZOr,}i[nfѳGWk9 Պ4`UaJJZRt<TOĿ;Hd920"pA\r:7$4ajV6ǒY20¶j9;Qꈸ2˸tyw/:a$,Tt'-Q{&=^-X@GNsl|>4[bYJBk -$jPYl/Z#_jphw8K| PEKσMB`9%rn=Rstn5^)Z9۟ -wD@rndDRXGo EkbOAb8U˛ dp/KqEÏkӧ&/3z?RiЪE#&ٛRA+عrXvGoɠRZEtv\t[ň[PvbZC|ZfbFpMnOTݑTա<-iꡈNʛyoME VuTrz+`n&%<.;T>QPJh2OSCš[NeE1)re,3N5A28"VX@} ư @XHG=?F+X -&PDOL)WZ78A27Q_0?h"Ejf6u[eq^O@=FwbknƋ,?kjK^~aXXFGK@\z{ss<}U캌 ʏrbQ;%)@4r@'tJP-;{OUrz4|Si`g-c&L@ b91)Y9ThsW*,&BN;O<ʆ$HCzkKQ?0=těG;cw5?X$ :~RPrn aőalAEԜp`hKf h6[)N/\RZHv#om~45{r9( -PBӆ)KwDozD\=ޢwD$Α -m`*; eRx:JZ_8º/ߊ~Z^ZnڼAn/*/ 7ROd g{J*f,$cn|xĖ(މUX*X}U\ffj2f@mBɺS!(l*>o*V,q k!JBF{&~39P@`a!PB!gdXy(aQޜt {xq@rPIq3Po86mhHA.qH$kʎebZm\N T` sUN!s^"SqݣFF*myw8qmfRP- Q 48 Dȱ3m ,!'=(%Bt&U;hf9iB*މQ-QY,VV,0JBجhEadxCM=)3QQАƍCADnB-N@&)3œ(N fk@9NGAYHo&4Dl6p7fI!_RA6Ti.7cA89BeAКOヲX,1ޘ2QrDf+|'oB E6.BIȦQ`*Sa_ -&VF۸]sBh۸Yͫmܸ(U0oB+XĤ6N6RH0%Jf !fPEL:o@L&aQ.v (+Cr{Q?jgv,+ u(e);d -Zu cub`'ЀL> -vYDՇibB)QBIlb> ̄ψ*2:RASp/m?Ȁ؝*P*A\HHI0e O1615q)j&j)̍'N˻l4G pI0OR*0Qj630P -<N*T -p^*{0df6nA$B){d&.%-R`e&h_ڸTHHС3[1Pid'Bg00yx .2TH*-\T}q #]!aZ&`U$R: 2ueuv4t/c8c@k32iygAPXxo2J0RLxiB0!a`c!jE<T۸ {_g}U8D]+HA 11P"P ) ]&lI.`;\efB$*#lj]X9KHȘSP5q-r5"q3^K <+@;@$1"J&? DQG&Sx}MkFL&;xPrZ Erppƃ%"pw\Y3+L$X xg 0Z oW`@$6Μ<{| ^'K>~#@ˉ+/s -FH){W؜'!2OMBC~5 Qiu/P|T -D"N -IaYPX #'FxHgayl!Y(ef+_ UrJD@B _(@ -'OT1R!'TK(崄JPTl+HqDgƉQpZ!@LBV32G@Rz<( $`8i!4ֶV}12I`C%!tIxS&H WQ012 $R7Y1$AP14#-_C|6Uː*` I)1 a7H:Qu ;Z#/'S,zFNT#e! m,٘`MfY=n&! -;{6g8gCATb?ke`/QƁqqYV x7ڎÄBolNDYd28pG:ދ{E]Ԇ6:HP?Z/).#jtqD8wp G (5dhTR m&JD"ơBÂE&ÌF[vck;j딝 -RqLDE"*cH  ]R!6Kc?fvWXFMɨ0VS=LBGA 4`pi&L4^7 cvYM` -`ԅLq0dFf €GY&2$@tpY>' ƩH3n)\m?7oDFG#̋ᲸZQMbas_u -.}-)7J@LKCbX.A+H~%ayXފO=wtYFmap/R!?|@xH䔩 R㱌0l,9BR؃(%-Q1^0=-/+>cHC~لc4 -t̎iH*̘.&BHVDu4j^<.Aa@$BtYBnz - I(J@d% hu0@~b钟XNwtFSJ R@uR).DMBQ BI0ecE$>Yc|woƄK:@;YG2 I""bmqKmq7fF@*CeT0RSg!B$G_&3:L~KW\`QeJ^5H@Rɘ4`4h:RŽD؂gF<`'28"](aW‘Qf%h 4{L48FѨzˌR/CPz .Ma9PxH`Qx'Vw@𘄌-a&.jkz/kks:o-{ÅZ[EzQ,vqv7Z7LgOqԊ+~8c\7}7Op+T[/zmc֚u"7N/ޔ}+~z[,:mƗ]_cmfwŻmo5gzE }flo.okVgQǟū/E쪿 _kz[i^>"u$/H@tm' -h& <EFØ=#|G wXyzWRl-cUrؗ\2e $1"rb, R9,^ djL,hhUx0#"MЙ *VB@-e 03ȅ=FHi4I2)ȇXQp0QA#M rSWB(D -lm<0섊6EJ!E#%* u)<ldRQyF6M<ꁆ钖T(ftm(J-Xh}̋9i6>2:q@lb2"+HgXz%Q,!8R 90R/Z߈@fZPaBj#ـy00:y@JBL0cY -x e@]4F0@tC,,qaIVH JThDaf赐4:xЦh&SPX -9)bɆH܈o.ffH0 8ٝ*vuF*t$Z4IP(}B8))prp/ R,=Pڀg[K< -IȄ$۸Ϫq,Aeo1s53AFH@g#ҙCI@K۸۸P6B3Z+ƍ$REeG (J}0hKӛ\1qz6.Sn!ay&& *MLP`E801AC?)gʎ*7g.,]jfFF8;(*/1gS/i$LMo,4J@~1xm*" LbX`JXEb+Z{RR\)YBvXwb[_|3xjg|^[]׍5⚳ƹJmyni_ʹ*nqx/]޼H3B]9SMj\oՋk5jO);q/g;W8?v|+WTocJUi/檭 fRE{yk\Y/roivo/4;wW)_1cfzk\oMiv}i,V[V͗ޫf|wzug{޵ps~~1ޏt/lq8zvq - _8|5{m: ]zt.f>"uUn뜩cNgѫƮ;pquu1o-,pc{fzӚ{{[yWL^T\9ETzŭ),(ۺ}~JvVT~>^yؾu.t:W}녦׵~}\0_9U*ntۋ+.ֽۿz Tt}ÅV[\i]|]V7w㜫h3Kzu/tco꽠xgVVwvR֮).(^޺L{VK7*~i_|ԊKY;pqŝ~Mu[,nWZ}W׿\7NE17\썯u{5~L~wz߫ŕϿL?oji; ]7_VBk7ݸ^5/o?N-~M[{w{TV}]{[R׻.Zةaچŷ ]p+u WM]_JRx W*4lョ5W"^ YٟZ|k:zx]iqxcQ׭w/ںgn6ߌhoܥR}ZݙjKEƩ[{޶޽EWzhMmZkiմ]3/USOo~zݢMbhziv7N/jkWqϷ7NUojڬ7zӍ<7N~7n3x:tߧ{U]lW1k7Nڼ1Z{[YqjiTXbN)343S00 I%4iP(F26"@ -@ -b uZAwq"Bfn%)[⡛OHWͬ_Mkݍ{!;5QKEK+m NQj if ou Rc垧n-fDk:?]!,RnN!wiqulTx&(G޶=Ta>Ru!xB7{GaJivܝE#1nD#9m{n|B☥s'ˏ=Ba,pTnq}O➍k䜂-.C?::IU]7 -FZ8bU h B>a?;}Tqᅰ%lXfj֪EDr!|<Ikz`UmuwvKKb6$.iyBʊܟ V;J)Hzsqp>j:ҽ~$V+r=jdUWjM`'^r A_Lڢ'_rs> o{l۩M li 4}RL@1[- i#My+on3VR|r6!*anF7#o^:jQzㅨĚ%Is k?.[邢J!xH# cjRvVAnalO̓ ݓ,hSxlE* 5(Ձr@8`7/ &;KFG9g:w&76F}}Cs?Gbt*Sa@9> FyMGL[=MiqC3$kR^?K&jLB 0URlM_{o1ɪ]K<ߢ($64WD,qN`2ݵD!c`~nQ"g*k!ţJ]y)ƪ gN<ۑm)Sdlh+R髐؀ӡd;;F6^dr^.]%0lLIS53 5K7UGǁK eT@B9RNA x\,13^G#_SI Gƻ/d/[&E *k\T!auu>H}j;A5oL7BS5֤_{xۤ%a#ѪveȄo# -pCpjDb@[Nw~륉?f ;JF3;G(>!8SY}oMkt6_v݊e"㽐#2l{}hLan|;.}moQ $|V464@6U4}3@zPAaf=Y䃣Q*h¸zVO[ZB ,`{C_ }+z{*PUz6*5 -DF2oהQP2ʃIk@2]=*K$ۿL;@&'H<@)_~Ne WueɿH+0:E!C췌 `x `q(_(u)5ڠ@%22{^^9gYF08#Im@q2Uо<X;+(>}ݦ cΘȠ^4O+bbFʎ5f((9Q!S4 b`sBPfFo()r\8BQ'چܷ;0 fD*k16`x_Ug?2VK!WS0V ]4FwbH. )F>6%bWSj cp*uX׎ g<qem%*=&s RG`& - -?`t -Y^,5+LԖf"Z%H}e(Τo -4 -ƮG=2вWkZpң:cTs8>_HuV' -2UI$J}8T4&3 ޴߉lPꚭz<J#9u tfiFMei9zIDP_0$Od&wߗACIW ܡЉX+owJ1r҄@ g`cM)U:7 -H. /S-FTuq/]!Uۗ|n8 +K OR{%(2K'Ԗ!j Ѯv" -4ݕÄ^Fu,I'Sԅ4ZQY䮭[l@awHۣRcЗZ;0#RΦv|z2q1b 1%5ŌTYu$0dI6pRm羇0THzͫ=?Aa {aut*IDՎ"m/ww\ӑ̜ (W(3`MOؐNhV"ޘ%24碜6%شS4&|RM<DwI~3EP'~E!"Mm`E<9I,A -xS3H'n\kqʦo;^% 5wƑ-9S=Ƭ(!S_8/>;"u'^rb͊7@3t)'{7VI 1oQ!$K9M -"!IZZՁg)nj]7طKUw F ߃dQv ^P_o$^\͚#˽ˆos:P1w n|`C= -$FⳢUQ4 -{؝l|2N"#16! -jAnz#. k/YTm$h˾U+*27Ew*Tmd%S-БXjgn΄!c'GTj֢2*2K01p TvU*5 -H| jdVKs_f]lcND#aiBERw]X7 KqluHvXk=.~pLDl FIGװHGj&iNW V\ -пTleFc>N͔ -6]S~uELjWA -lR)Cy'U5|WPId;"zB&#9x"L `BXsnR@tA:77k?hZ\l=:=?%}D=yI>ϊۨeΕcC,[MܾBr=vl3I\p^~UU^OPhD=i -qa57/s?#^Y6epnSxd *258rRߵ-ENH)j L T+ʅ_2eU8pTiVy3psUۥvDglV'X#Ws6")mg.2 WVKA2o$TH ]aBxԗ0peMPa񦌕)_< hhD }K]8ئLov\l޺R*TXp}ywLbA EciUb:u0P>fnLXMWi)SkBd{|" =j}ؤГtZ TM}Gg -[Wb?NQ5cå1.oE4H0|!įI쩤wqD s0yr@~:ns+oF}%,/3HoeWnRr'{qҵ&5ֱBf̱8i6]b=}੖! -r;^T\Ғ]F.rZٲ +||EY$|)EA;fna7퐓7 [ ØŢoXb 7f*蟒mzlcwFѴfZfeˊf$)v6,ɼVlNeY'ijly kYv$_j+ ,kI=VwYExnYSԌ2P, 7ɸq@dްT3 6 ǵ3XF^+ p!v), (pF_Is %iHʫyfOp S?*E>j2$gj>ɷ%Xa4&?7Mx JrqLNTx=:IlUF] zj+ C/:l^+S;e()ͨsj}(9V*ƞWPvpŔG[4y7E 訔ڈm {cr!3bXX/R.T|6,?W?1*˒C!\0zy -<FNN|_he;#־ٿ`{]h(QY~b=00tJ*!?^+k "6'*W,¨S'DC| -*Aj͍ ]az_^ #)zUaf!p:Q4JRIL31bt/[%<dLk1&V"ĸWrULKS:kOg2&BAtF-p7SxWO*lu!62Xro$u@O$iL:`|E084./~PkS$Dž%6!w]%%nxlu9Q7'meq5Vkеpxg8@SiIU/Z+ZywPJѐ4 -cYw{&ID"5W$&Z5&xmJ)3~b2̲B}j[Y\W";!194*_ hckk^q&rZϸ)A02L.g pOAmuT'Rn Ub|HN8Jl7bvH! E|@ُ6JG):>.*6vE3o-}F0ӒdXнF>ڻй y:x D ȻZgJʗ5)Dwiv]GFwh_Btv_'ډN<13p@͞p-0;ha=ӣG\n(S .-@7% Oݵit_:0NB܌t׃b5&)61CX._ݕп*`E.d6z)"MPWR\Kd fgɍY ޅLI4V$suJ৓qw;IBe5SpP7HP@I?ӣ&ˈ 8B7}>\4zh. -n+}!{pCP"nFEI軅 <1($RMUH`-LH("SSBEf$iQ=80-^l; -f$uwnuZ=UxwqI,z5%4[`SeK[]+}C3ajF=ojNjӝA;ЗBn6,Yb^bx6t,41Z6DcÖW7ĺ_/@G ˥Ϋҝ`y{x5#}[ MHB"W H~1Ӭ;B62Tw!a 5$s?Իj(oi297>KԘn2Gݨ!!\tY:bl ɷM&or(0l - ͌N3,s"i?̰d ⳤl:؆4Sa4[P1V[q!@UVJ$mX8ykBjV6y_0&($0-pk҂Ɇ/U|+zo ' jia.Bs)кez-U~\HjC+DB+NmzBAڳ ۂ4Y l um0ıejo.lT*GEѪ91lDHG",(is_v Z}&G'`] -؀KoJKқJ -Rw%ɽ n.; V_٤ Qm{w0190^,?zJn & I|m/ 7*Ye ?^ -V@O!.޼G`@ 7Ԉ&uܼ(4hDSȒ_z Pg6%tᰫJeB7GɅ/rY+Eĵ9{j"J| -4pn8\ x5QpgTh* ЃP5Ǖ edPR&S+vuܻ1o)}n$<_j/qMp7eMҾ+j%SWۄΕ7#y-60> hDa Dq֚Aar'"FRu -)q䦣PmwW=]5[Dx74ͪ ېM4j@sFuj*ENK?0Bc`]ܛ %3zWGH} Af~naQ2f0/-jkNptlP;`O~B%(1E0)`P\*`pz]?0W%i7(279!ԫ2&9t>Izg 9^`1UFfV˶Amo(Y]BiXpV8ȄIg"7Ҥ+H1)?^v:&fi¥ԁt48FI[\a*~2hRaozPiD# -tOAl+/ OɎeI%.:h^e4sf B]D\w3!!Əz) -tUpJ~fmor=*Cx]췒VR#R:*TC9;7%vpfji@&sϐ)NXs/VYk[yCIr#㒻d\.CF]b2A/ q[2#=r,)D/HRlׂ.8|DvK~_o%N~57tEdcFM6H6=+,{( -r/D;fW[;l ,XxfMLj -endstream endobj 30 0 obj [/Indexed/DeviceRGB 255 31 0 R] endobj 31 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX$6Ra!<<'!!!*'!!rrmPX()~> -endstream endobj 24 0 obj <> endobj 32 0 obj [/View/Design] endobj 33 0 obj <>>> endobj 28 0 obj <> endobj 27 0 obj [/ICCBased 34 0 R] endobj 34 0 obj <>stream -HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  - 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 -V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= -x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- -ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 -N')].uJr - wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 -n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! -zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km -endstream endobj 25 0 obj [24 0 R] endobj 35 0 obj <> endobj xref -0 36 -0000000004 65535 f -0000000016 00000 n -0000000147 00000 n -0000013733 00000 n -0000000000 00000 f -0000013784 00000 n -0000000000 00000 f -0000000000 00000 f -0000016323 00000 n -0000016395 00000 n -0000016634 00000 n -0000018275 00000 n -0000083864 00000 n -0000149453 00000 n -0000215042 00000 n -0000280631 00000 n -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000304029 00000 n -0000307022 00000 n -0000014175 00000 n -0000304329 00000 n -0000304216 00000 n -0000015941 00000 n -0000303454 00000 n -0000303502 00000 n -0000304100 00000 n -0000304131 00000 n -0000304364 00000 n -0000307047 00000 n -trailer -<<8E9FFD9F61D2424E89A75D8D75A99DA7>]>> -startxref -307229 -%%EOF diff --git a/data/tr2/logo.png b/data/tr2/logo.png new file mode 100644 index 000000000..cc49cc56a Binary files /dev/null and b/data/tr2/logo.png differ diff --git a/data/tr2/ship/cfg/TR2X_gameflow.json5 b/data/tr2/ship/cfg/TR2X_gameflow.json5 index ead0d9f3b..5cfda3031 100644 --- a/data/tr2/ship/cfg/TR2X_gameflow.json5 +++ b/data/tr2/ship/cfg/TR2X_gameflow.json5 @@ -2,8 +2,9 @@ // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. - "main_menu_picture": "data/images/title_eu.png", + "main_menu_picture": "data/images/title_eu.webp", "savegame_fmt_legacy": "savegame.%d", + "savegame_fmt_bson": "save_tr2_%02d.dat", "cmd_init": {"action": "exit_to_title"}, "cmd_title": {"action": "noop"}, @@ -27,7 +28,7 @@ "path": "data/title.tr2", "music_track": 64, "sequence": [ - {"type": "display_picture", "path": "data/images/legal.png"}, + {"type": "display_picture", "path": "data/images/legal_eu.webp", "legal": true}, {"type": "play_fmv", "fmv_id": 0}, {"type": "play_fmv", "fmv_id": 1}, {"type": "exit_to_title"}, @@ -84,6 +85,7 @@ {"type": "level_complete"}, ], "injections": [ + "data/injections/boat_bits.bin", "data/injections/common_pickup_meshes.bin", ], }, @@ -232,6 +234,7 @@ {"type": "level_complete"}, ], "injections": [ + "data/injections/deck_fd.bin", "data/injections/deck_itemrots.bin", "data/injections/deck_pickup_meshes.bin", "data/injections/living_deck_goon_sfx.bin", @@ -308,6 +311,7 @@ ], "injections": [ "data/injections/common_pickup_meshes.bin", + "data/injections/guardian_death_commands.bin", "data/injections/palace_fd.bin", "data/injections/palace_itemrots.bin", ], @@ -382,15 +386,15 @@ {"type": "loop_game"}, {"type": "play_music", "music_track": 52}, {"type": "level_complete"}, - {"type": "display_picture", "path": "data/images/credit01.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit02.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit03.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit04.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit05.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit06.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit07.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit08.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "total_stats", "background_path": "data/images/end.png"}, + {"type": "display_picture", "path": "data/images/credit01.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit02.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit03.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit04.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit05.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit06.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit07.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit08.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "total_stats", "background_path": "data/images/end.webp"}, ], "injections": [ "data/injections/house_itemrots.bin", @@ -485,13 +489,14 @@ {"type": "loop_game"}, ], "injections": [ + "data/injections/cut4_textures.bin", "data/injections/photo.bin", ], }, ], "fmvs": [ - {"path": "fmv/LOGO.RPL"}, + {"path": "fmv/LOGO.RPL", "legal": true}, {"path": "fmv/ANCIENT.RPL"}, {"path": "fmv/MODERN.RPL"}, {"path": "fmv/LANDING.RPL"}, diff --git a/data/tr2/ship/cfg/TR2X_gameflow_gm.json5 b/data/tr2/ship/cfg/TR2X_gameflow_gm.json5 index afcb1544f..e5a2b9b07 100644 --- a/data/tr2/ship/cfg/TR2X_gameflow_gm.json5 +++ b/data/tr2/ship/cfg/TR2X_gameflow_gm.json5 @@ -2,8 +2,9 @@ // NOTE: bad changes to this file may result in crashes. // Lines starting with double slashes are comments and are ignored. - "main_menu_picture": "data/images/title_eu_gm.png", + "main_menu_picture": "data/images/title_eu_gm.webp", "savegame_fmt_legacy": "savegame_gm.%d", + "savegame_fmt_bson": "save_trgm_%02d.dat", "cmd_init": {"action": "exit_to_title"}, "cmd_title": {"action": "noop"}, @@ -27,7 +28,7 @@ "path": "data/title_gm.tr2", "music_track": 64, "sequence": [ - {"type": "display_picture", "path": "data/images/legal.png"}, + {"type": "display_picture", "path": "data/images/legal_eu_gm.webp", "legal": true}, {"type": "exit_to_title"}, ], }, @@ -59,6 +60,10 @@ {"type": "level_stats"}, {"type": "level_complete"}, ], + "injections": [ + "data/injections/common_pickup_meshes.bin", + "data/injections/shark_sfx.bin", + ], }, // 2. Fool's Gold @@ -71,6 +76,9 @@ {"type": "level_stats"}, {"type": "level_complete"}, ], + "injections": [ + "data/injections/fools_pickup_meshes.bin", + ], }, // 3. Furnace of the Gods @@ -83,6 +91,9 @@ {"type": "level_stats"}, {"type": "level_complete"}, ], + "injections": [ + "data/injections/furnace_pickup_meshes.bin", + ], }, // 4. Kingdom @@ -93,18 +104,22 @@ {"type": "give_item", "object_id": "puzzle_1"}, {"type": "loop_game"}, {"type": "play_music", "music_track": 52}, - {"type": "display_picture", "path": "data/images/credit00_gm.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit01.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit02.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit03.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit04.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit05.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit06.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit07_gm.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "display_picture", "path": "data/images/credit08.png", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, - {"type": "total_stats", "background_path": "data/images/end.png"}, + {"type": "display_picture", "path": "data/images/credit00_gm.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit01.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit02.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit03.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit04.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit05.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit06.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit07_gm.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "display_picture", "path": "data/images/credit08.webp", "display_time": 15, "fade_in_time": 0.5, "fade_out_time": 0.5}, + {"type": "total_stats", "background_path": "data/images/end.webp"}, {"type": "level_complete"}, ], + "injections": [ + "data/injections/common_pickup_meshes.bin", + "data/injections/guardian_death_commands.bin", + ], }, // 5. Nightmare in Vegas @@ -123,6 +138,11 @@ {"type": "level_stats"}, {"type": "level_complete"}, ], + "injections": [ + "data/injections/common_pickup_meshes.bin", + "data/injections/guardian_death_commands.bin", + "data/injections/vegas_fd.bin", + ], }, ], diff --git a/data/tr2/ship/cfg/TR2X_gameflow_level.json5 b/data/tr2/ship/cfg/TR2X_gameflow_level.json5 index ee7a43d75..5eaf8eab2 100644 --- a/data/tr2/ship/cfg/TR2X_gameflow_level.json5 +++ b/data/tr2/ship/cfg/TR2X_gameflow_level.json5 @@ -1,8 +1,9 @@ { // This file is used to enable the -l argument support. - "main_menu_picture": "data/images/title_eu.png", - "savegame_fmt_legacy": "savegame.%d", + "main_menu_picture": "data/images/title_eu.webp", + "savegame_fmt_legacy": "savegame_custom.%d", + "savegame_fmt_bson": "save_tr2_custom_%02d.dat", "cmd_init": {"action": "exit_to_title"}, "cmd_title": {"action": "noop"}, diff --git a/data/tr2/ship/cfg/TR2X_strings.json5 b/data/tr2/ship/cfg/TR2X_strings.json5 index 65078e21f..3be3b698f 100644 --- a/data/tr2/ship/cfg/TR2X_strings.json5 +++ b/data/tr2/ship/cfg/TR2X_strings.json5 @@ -464,13 +464,43 @@ }, "game_strings": { - "CONTROL_BACKEND_CONTROLLER": "Controller", - "CONTROL_BACKEND_KEYBOARD": "Keyboard", - "CONTROL_CUSTOMIZE": "Customize Controls", - "CONTROL_CUSTOM_1": "User Keys 1", - "CONTROL_CUSTOM_2": "User Keys 2", - "CONTROL_CUSTOM_3": "User Keys 3", - "CONTROL_DEFAULT_KEYS": "Default Keys", + "CONTROLS_BACKEND_CONTROLLER": "Controller", + "CONTROLS_BACKEND_KEYBOARD": "Keyboard", + "CONTROLS_CUSTOMIZE": "Customize Controls", + "CONTROLS_CUSTOM_1": "User Keys 1", + "CONTROLS_CUSTOM_2": "User Keys 2", + "CONTROLS_CUSTOM_3": "User Keys 3", + "CONTROLS_DEFAULT_KEYS": "Default Keys", + "DETAIL_ASPECT_MODE": "Aspect mode", + "DETAIL_ASPECT_MODE_16_9": "16:9", + "DETAIL_ASPECT_MODE_4_3": "4:3", + "DETAIL_ASPECT_MODE_ANY": "Any", + "DETAIL_BILINEAR": "Bilinear", + "DETAIL_DEPTH_BUFFER": "Z-Buffer", + "DETAIL_FLOAT_FMT": "%.1f", + "DETAIL_FOG_END": "Fog end", + "DETAIL_FOG_START": "Fog start", + "DETAIL_FOV": "Field of view", + "DETAIL_FPS": "FPS", + "DETAIL_INTEGER_FMT": "%d", + "DETAIL_LIGHTING_CONTRAST": "Lighting contrast", + "DETAIL_LIGHTING_CONTRAST_HIGH": "High", + "DETAIL_LIGHTING_CONTRAST_LOW": "Low", + "DETAIL_LIGHTING_CONTRAST_MEDIUM": "Medium", + "DETAIL_RENDER_MODE": "Render mode", + "DETAIL_RENDER_MODE_HARDWARE": "Hardware", + "DETAIL_RENDER_MODE_SOFTWARE": "Software", + "DETAIL_SCALER": "Scaler", + "DETAIL_SIZER": "Sizer", + "DETAIL_TEXTURE_FILTER": "Texture filter", + "DETAIL_TITLE": "Graphic Options", + "DETAIL_TRAPEZOID_FILTER": "Trapezoid filter", + "DETAIL_UI_BAR_SCALE": "UI bar scale", + "DETAIL_UI_TEXT_SCALE": "UI text scale", + "DETAIL_USE_PSX_FOV": "Use PSX FOV", + "DETAIL_WATER_COLOR_B": "Water color (B)", + "DETAIL_WATER_COLOR_G": "Water color (G)", + "DETAIL_WATER_COLOR_R": "Water color (R)", "HEADING_GAME_OVER": "GAME OVER", "HEADING_INVENTORY": "INVENTORY", "HEADING_ITEMS": "ITEMS", @@ -505,7 +535,7 @@ "KEYMAP_USE_FLARE": "Flare", "KEYMAP_WALK": "Walk", "MISC_DEMO_MODE": "Demo Mode", - "MISC_EMPTY_SLOT": "- EMPTY SLOT -", + "MISC_EMPTY_SLOT_FMT": "- EMPTY SLOT -", "MISC_EXIT": "Exit", "MISC_NONE": "None", "MISC_OFF": "Off", @@ -591,13 +621,22 @@ "OSD_UNKNOWN_COMMAND": "Unknown command: %s", "OSD_WIREFRAME_MODE_OFF": "Wireframe mode: off", "OSD_WIREFRAME_MODE_ON": "Wireframe mode: on", + "PAGINATION_NAV": "%d / %d", "PASSPORT_EXIT_DEMO": "Exit Demo", "PASSPORT_EXIT_GAME": "Exit Game", "PASSPORT_EXIT_TO_TITLE": "Exit to Title", + "PASSPORT_LEGACY_SELECT_LEVEL_1": "Legacy saves do not", + "PASSPORT_LEGACY_SELECT_LEVEL_2": "support this feature.", "PASSPORT_LOAD_GAME": "Load Game", + "PASSPORT_MODE_NEW_GAME": "New Game", + "PASSPORT_MODE_NEW_GAME_JP": "Japanese NG", + "PASSPORT_MODE_NEW_GAME_JP_PLUS": "Japanese NG+", + "PASSPORT_MODE_NEW_GAME_PLUS": "New Game+", "PASSPORT_NEW_GAME": "New Game", "PASSPORT_SAVE_GAME": "Save Game", "PASSPORT_SELECT_LEVEL": "Select Level", + "PASSPORT_SELECT_MODE": "Select Mode", + "PASSPORT_STORY_SO_FAR": "Story so far...", "PAUSE_ARE_YOU_SURE": "Are you sure?", "PAUSE_CONTINUE": "Continue", "PAUSE_EXIT_TO_TITLE": "Exit to title?", diff --git a/data/tr2/ship/data/images/credit00_gm.png b/data/tr2/ship/data/images/credit00_gm.png deleted file mode 100644 index 77ec2d4b8..000000000 Binary files a/data/tr2/ship/data/images/credit00_gm.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit01.png b/data/tr2/ship/data/images/credit01.png deleted file mode 100644 index 769e873ae..000000000 Binary files a/data/tr2/ship/data/images/credit01.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit02.png b/data/tr2/ship/data/images/credit02.png deleted file mode 100644 index aa0d11f92..000000000 Binary files a/data/tr2/ship/data/images/credit02.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit03.png b/data/tr2/ship/data/images/credit03.png deleted file mode 100644 index 50c503aaa..000000000 Binary files a/data/tr2/ship/data/images/credit03.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit04.png b/data/tr2/ship/data/images/credit04.png deleted file mode 100644 index 2fe9cd7ec..000000000 Binary files a/data/tr2/ship/data/images/credit04.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit05.png b/data/tr2/ship/data/images/credit05.png deleted file mode 100644 index dd7b323aa..000000000 Binary files a/data/tr2/ship/data/images/credit05.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit06.png b/data/tr2/ship/data/images/credit06.png deleted file mode 100644 index 96ab1fdc1..000000000 Binary files a/data/tr2/ship/data/images/credit06.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit07.png b/data/tr2/ship/data/images/credit07.png deleted file mode 100644 index 0fbcb0d47..000000000 Binary files a/data/tr2/ship/data/images/credit07.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit07_gm.png b/data/tr2/ship/data/images/credit07_gm.png deleted file mode 100644 index 08a3b977c..000000000 Binary files a/data/tr2/ship/data/images/credit07_gm.png and /dev/null differ diff --git a/data/tr2/ship/data/images/credit08.png b/data/tr2/ship/data/images/credit08.png deleted file mode 100644 index 9cec2610c..000000000 Binary files a/data/tr2/ship/data/images/credit08.png and /dev/null differ diff --git a/data/tr2/ship/data/images/end.png b/data/tr2/ship/data/images/end.png deleted file mode 100644 index 1bfa824fc..000000000 Binary files a/data/tr2/ship/data/images/end.png and /dev/null differ diff --git a/data/tr2/ship/data/images/legal.png b/data/tr2/ship/data/images/legal.png deleted file mode 100644 index b4f307df6..000000000 Binary files a/data/tr2/ship/data/images/legal.png and /dev/null differ diff --git a/data/tr2/ship/data/images/title_eu.png b/data/tr2/ship/data/images/title_eu.png deleted file mode 100644 index 10fc2ef85..000000000 Binary files a/data/tr2/ship/data/images/title_eu.png and /dev/null differ diff --git a/data/tr2/ship/data/images/title_eu_gm.png b/data/tr2/ship/data/images/title_eu_gm.png deleted file mode 100644 index a36eb1100..000000000 Binary files a/data/tr2/ship/data/images/title_eu_gm.png and /dev/null differ diff --git a/data/tr2/ship/data/images/title_us.png b/data/tr2/ship/data/images/title_us.png deleted file mode 100644 index 84ecfdbfa..000000000 Binary files a/data/tr2/ship/data/images/title_us.png and /dev/null differ diff --git a/data/tr2/ship/data/images/title_us_gm.png b/data/tr2/ship/data/images/title_us_gm.png deleted file mode 100644 index b2bbdbea9..000000000 Binary files a/data/tr2/ship/data/images/title_us_gm.png and /dev/null differ diff --git a/data/tr2/ship/data/injections/barkhang_pickup_meshes.bin b/data/tr2/ship/data/injections/barkhang_pickup_meshes.bin index 99284528c..6258d8387 100644 Binary files a/data/tr2/ship/data/injections/barkhang_pickup_meshes.bin and b/data/tr2/ship/data/injections/barkhang_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/boat_bits.bin b/data/tr2/ship/data/injections/boat_bits.bin new file mode 100644 index 000000000..dbf348b22 Binary files /dev/null and b/data/tr2/ship/data/injections/boat_bits.bin differ diff --git a/data/tr2/ship/data/injections/common_pickup_meshes.bin b/data/tr2/ship/data/injections/common_pickup_meshes.bin index 08b219154..7f89d0265 100644 Binary files a/data/tr2/ship/data/injections/common_pickup_meshes.bin and b/data/tr2/ship/data/injections/common_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/cut4_textures.bin b/data/tr2/ship/data/injections/cut4_textures.bin new file mode 100644 index 000000000..372e9444f Binary files /dev/null and b/data/tr2/ship/data/injections/cut4_textures.bin differ diff --git a/data/tr2/ship/data/injections/deck_fd.bin b/data/tr2/ship/data/injections/deck_fd.bin new file mode 100644 index 000000000..4f56521cb Binary files /dev/null and b/data/tr2/ship/data/injections/deck_fd.bin differ diff --git a/data/tr2/ship/data/injections/deck_pickup_meshes.bin b/data/tr2/ship/data/injections/deck_pickup_meshes.bin index 81bf404b3..08792874c 100644 Binary files a/data/tr2/ship/data/injections/deck_pickup_meshes.bin and b/data/tr2/ship/data/injections/deck_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/diving_pickup_meshes.bin b/data/tr2/ship/data/injections/diving_pickup_meshes.bin index 91cddc638..f53d8423b 100644 Binary files a/data/tr2/ship/data/injections/diving_pickup_meshes.bin and b/data/tr2/ship/data/injections/diving_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/floating_fd.bin b/data/tr2/ship/data/injections/floating_fd.bin index d1f98768a..044a0d4e8 100644 Binary files a/data/tr2/ship/data/injections/floating_fd.bin and b/data/tr2/ship/data/injections/floating_fd.bin differ diff --git a/data/tr2/ship/data/injections/floating_pickup_meshes.bin b/data/tr2/ship/data/injections/floating_pickup_meshes.bin index d994fd149..f38dd3d5d 100644 Binary files a/data/tr2/ship/data/injections/floating_pickup_meshes.bin and b/data/tr2/ship/data/injections/floating_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/font.bin b/data/tr2/ship/data/injections/font.bin index f7127feae..cc0e17b37 100644 Binary files a/data/tr2/ship/data/injections/font.bin and b/data/tr2/ship/data/injections/font.bin differ diff --git a/data/tr2/ship/data/injections/fools_pickup_meshes.bin b/data/tr2/ship/data/injections/fools_pickup_meshes.bin new file mode 100644 index 000000000..ef8f32db2 Binary files /dev/null and b/data/tr2/ship/data/injections/fools_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/furnace_pickup_meshes.bin b/data/tr2/ship/data/injections/furnace_pickup_meshes.bin new file mode 100644 index 000000000..7e99c9691 Binary files /dev/null and b/data/tr2/ship/data/injections/furnace_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/guardian_death_commands.bin b/data/tr2/ship/data/injections/guardian_death_commands.bin new file mode 100644 index 000000000..70801064d Binary files /dev/null and b/data/tr2/ship/data/injections/guardian_death_commands.bin differ diff --git a/data/tr2/ship/data/injections/living_pickup_meshes.bin b/data/tr2/ship/data/injections/living_pickup_meshes.bin index 07146c093..97c03c924 100644 Binary files a/data/tr2/ship/data/injections/living_pickup_meshes.bin and b/data/tr2/ship/data/injections/living_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/rig_pickup_meshes.bin b/data/tr2/ship/data/injections/rig_pickup_meshes.bin index 9c07f6e76..fc7a2bf89 100644 Binary files a/data/tr2/ship/data/injections/rig_pickup_meshes.bin and b/data/tr2/ship/data/injections/rig_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/shark_sfx.bin b/data/tr2/ship/data/injections/shark_sfx.bin new file mode 100644 index 000000000..cea287175 Binary files /dev/null and b/data/tr2/ship/data/injections/shark_sfx.bin differ diff --git a/data/tr2/ship/data/injections/vegas_fd.bin b/data/tr2/ship/data/injections/vegas_fd.bin new file mode 100644 index 000000000..ee24306c2 Binary files /dev/null and b/data/tr2/ship/data/injections/vegas_fd.bin differ diff --git a/data/tr2/ship/data/injections/wreck_pickup_meshes.bin b/data/tr2/ship/data/injections/wreck_pickup_meshes.bin index 75aef0525..25327bd94 100644 Binary files a/data/tr2/ship/data/injections/wreck_pickup_meshes.bin and b/data/tr2/ship/data/injections/wreck_pickup_meshes.bin differ diff --git a/data/tr2/ship/data/injections/xian_fd.bin b/data/tr2/ship/data/injections/xian_fd.bin index d98701465..258aac7dc 100644 Binary files a/data/tr2/ship/data/injections/xian_fd.bin and b/data/tr2/ship/data/injections/xian_fd.bin differ diff --git a/data/tr2/ship/data/injections/xian_pickup_meshes.bin b/data/tr2/ship/data/injections/xian_pickup_meshes.bin index 70029abf7..2b47b6493 100644 Binary files a/data/tr2/ship/data/injections/xian_pickup_meshes.bin and b/data/tr2/ship/data/injections/xian_pickup_meshes.bin differ diff --git a/data/tr2/ship/music/57.mp3 b/data/tr2/ship/music/57.mp3 deleted file mode 100644 index ad9e2b673..000000000 Binary files a/data/tr2/ship/music/57.mp3 and /dev/null differ diff --git a/docs/GAME_FLOW.md b/docs/GAME_FLOW.md index 2a173a8d0..2d48e919f 100644 --- a/docs/GAME_FLOW.md +++ b/docs/GAME_FLOW.md @@ -34,8 +34,8 @@ remains distinct for each game. "savegame_fmt_bson": "save_tr1_%02d.dat", "demo_delay": 16, "water_color": [0.45, 1.0, 1.0], -"draw_distance_fade": 22.0, -"draw_distance_max": 30.0, +"fog_start": 22.0, +"fog_end": 30.0, "injections": [ "data/global_injection1.bin", "data/global_injection2.bin", @@ -98,14 +98,14 @@ remains distinct for each game. - draw_distance_fade + fog_start - Double* + Double The distance (in tiles) at which objects and the world start to fade into blackness.

    -
  • The default hardcoded value in TR1 is 12.
  • +
  • The default value in OG TR1 is hardcoded to 12.
  • The default (disabled) value in TombATI is 72.
@@ -113,13 +113,13 @@ remains distinct for each game. - draw_distance_max + fog_end - Double* + Double The distance (in tiles) at which objects and the world are clipped away.
    -
  • The default hardcoded value in TR1 is 20.
  • +
  • The default value in OG TR1 is hardcoded to 20.
  • The default (disabled) value in TombATI is 80.
@@ -196,11 +196,9 @@ remains distinct for each game. Float array - Water color (R, G, B). 1.0 means pass-through, 0.0 means no value at all. -
    -
  • [0.6, 0.7, 1.0] is the original DOS version filter.
  • -
  • [0.45, 1.0, 1.0] is the default TombATI filter.
  • -
+ Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0 + or `00` means completely black color. + See this table for reference values. @@ -315,6 +313,39 @@ remains distinct for each game. The path to the sound effects (.sfx) file to use in the game. + + + + fog_start + + Double + + The distance (in tiles) at which objects and the world start to fade into + blackness. The default value in OG TR2 is hardcoded to 12. + + + + + + fog_end + + Double + + The distance (in tiles) at which objects and the world are clipped away. + The default value in OG TR2 is hardcoded to 20. + + + + + water_color + + Float array or hex string + + Water color (R, G, B) or `#RRGGBB`. 1.0 or `FF` means pass-through, 0.0 + or `00` means completely black color. + See this table for reference values. + + ## Game flow commands @@ -424,8 +455,8 @@ Following are each of the properties available within a level. "music_track": 57, "lara_type": 0, "water_color": [0.7, 0.5, 0.85], - "draw_distance_fade": 34.0, - "draw_distance_max": 50.0, + "fog_start": 34.0, + "fog_end": 50.0, "unobtainable_pickups": 1, "unobtainable_kills": 1, "inherit_injections": false, @@ -517,7 +548,7 @@ Following are each of the properties available within a level. The ambient music track ID. - draw_distance_fade¹ + fog_start Double Can be customized per level. See above @@ -525,7 +556,7 @@ Following are each of the properties available within a level. - draw_distance_max¹ + fog_end Double Can be customized per level. See above @@ -659,7 +690,18 @@ default game flow for examples. path String - Displays the specified picture for a fixed time. + + 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. + display_time @@ -1184,17 +1226,27 @@ best practice to remove the references to maintain a clean game flow file. Following is a summary of what each of the default injection files that are provided with the game achieves. -#### TR1 - + + + + + + + + + + + + + + + + + + + + + + + + @@ -1221,16 +1305,28 @@ provided with the game achieves. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1345,17 +1489,23 @@ provided with the game achieves. + + + + + +
Injection fileUsage Purpose
+ *_cameras.bin + TR1 + Injects positional adjustments for cameras that can otherwise cause visual + issues, such as in Temple of the Cat. +
*_fd.bin TR1, TR2 Injects fixes for floor data issues in the original levels. Refer to the README for a full list of fixes. @@ -1204,15 +1256,47 @@ provided with the game achieves. *_itemrots.bin TR1, TR2 Injects rotations on pickup items so they make more visual sense when using the 3D pickups option.
+ *_meshfixes.bin + TR1 + Injects miscellaneous mesh adjustments for objects, such as in Obelisk of + Khamoon to avoid z-fighting. +
+ *_pickup_meshes.bin + TR1, TR2 + Injects mesh edits to change the scale of various pickup models, such as + the keys in St. Francis' Folly or the Prayer Wheel in Barkhang Monastry. +
+ *_sfx.bin + TR1, TR2 + Injects various SFX fixes or additions, such as the PSX Uzi SFX in TR1, or + fixing the silent enemies in TR2's water levels. +
*_skybox.bin TR1 Injects a predefined skybox model into specific levels. *_textures.bin TR1 Injects fixes for texture issues in the original levels, such as gaps in the walls or wrongly colored models. Refer to the README for a full list of fixes.
+ boat_bits.bin + TR2 + Injects a model in slot `O_BOAT_BITS` (221) which is used to show the boat + exploding when it crosses mines. +
backpack.bin TR1 Injects mesh edits for Lara's backback, such that it becomes shallower. This is only applied when the braid is enabled, to avoid the braid @@ -1247,6 +1343,7 @@ provided with the game achieves. braid.bin TR1 Injects a braid when the option for it is enabled. This also edits Lara's head meshes (object 0 and object 4) to make the braid fit better. A golden @@ -1270,10 +1367,21 @@ provided with the game achieves. braid_valley.bin
+ bubbles.bin + TR1 + Injects replacement sprite textures for Lara's underwater bubble sprites, + which are cut off in OG. +
cistern_plants.bin TR1 This disables the animation on sprite ID 193 in The Cistern and Tomb of Tihocan. @@ -1283,6 +1391,7 @@ provided with the game achieves. khamoon_mummy.bin TR1 Injects the mummy in room 25 of City of Khamoon, which is present in the PS1 version but not the PC. @@ -1292,23 +1401,55 @@ provided with the game achieves. lara_animations.bin TR1 Injects several animations, state changes and commands for Lara, such as responsive jumping, jump-twist, somersault, underwater roll, and wading.
+ lara_gym_guns.bin + TR1 + Injects all of Lara's weapons and weapon animations in TR1's gym level. +
explosion.bin TR1 Injects explosion sprites for certain console commands.
+ font.bin + TR1, TR2 + Injects replacement font sprites to support more characters than OG. +
+ guardian_death_commands.bin + TR2 + Injects an animation command for the bird guardian to end the level on the + final frame of its death animation. The original hard-coded end-level + behaviour is removed in TR2X. +
mines_pushblocks.bin TR1 Injects animation command data for pushblock types 2, 3 and 4 to restore the missing scraping SFX when pulling these blocks. @@ -1318,6 +1459,7 @@ provided with the game achieves. pickup_aid.bin TR1 Injects a sprite sequence similar to the Midas twinkle effect, which is used when the option for pickup aids is enabled. Custom levels should @@ -1328,6 +1470,7 @@ provided with the game achieves. photo.bin TR1, TR2 Injects camera shutter sound effect for the photo mode, needed only for the cutscene levels. @@ -1337,6 +1480,7 @@ provided with the game achieves. purple_crystal.bin TR1 Injects a replacement savegame crystal model to match the PS1 style. scion_collision.bin TR1 Increases the collision radius on the (targetable) Scion such that it can be shot with the shotgun.
+ seaweed_collision.bin + TR2 + Fixes the seaweed in Living Quarters blocking Lara from exiting the water. +
-#### TR2 - -TBD - ## FMVs The FMVs section of the document defines how to play video content. This is an @@ -1422,3 +1572,65 @@ keyboard or controller layouts defined. If you do not have any requirement to enforce settings, you can omit the `enforced_config` section from your game flow altogether. + +## Water colors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GameColor (hex)Color (array)Usage
TR1 #99B2FF[0.6, 0.7, 1.0]original DOS version color
#72FFFF[0.45, 1.0, 1.0]default TombATI color
TR2 #80DFFF[0.5, 0.875, 1.0]default PC hardware renderer color
#AAAAFF[0.66, 0.66, 1.0]default PC software renderer color
#CCFF80[0.8, 1.0, 0.5]Venice, Bartoli's Hideout and Opera House (PS1)
#CCFF99[0.8, 1.0, 0.6]Temple of Xian (PS1)
#CCFFCC[0.8, 1.0, 0.8]Floating Islands and Dragon's Lair (PS1)
#B2E5E5[0.7, 0.9, 0.9]The Great Wall and Tibetan Foothills (PS1)
#80FFFF[0.5, 1.0, 1.0]All other PS1 levels
diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 0b7232906..aa61e581f 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -2,7 +2,17 @@ ## TR1X -### Version 4.7 to TR1X 4.8 +### Version 4.9 to 4.10 + +1. **Update fog configuration** + If you wish to force your fog settings on player: + - Rename `draw_distance_min` to `fog_start` + - Rename `draw_distance_max` to `fog_end` + If you wish to give the player agency to change the fog: + - Remove `draw_distance_min` and `draw_distance_max` + + +### Version 4.7 to 4.8 1. **Rename basic keys** - Replace `file` key with `path` for every level. diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index db0561503..540e34371 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -1,4 +1,42 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr1-4.9...develop) - ××××-××-×× +- added an ability to customize the fog distances (#634) +- added an ability to customize the water color [see the reference](/docs/GAME_FLOW.md#water-color-table) (#1532) +- added support for a hex water color notation (eg. `#80FFFF`) in the game flow file +- added support for antitriggers, like TR2+ (#2580) +- added support for aspect ratio-specific images (#1840) +- changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end` +- changed `Select Detail` dialog title to `Graphic Options` +- changed the number of static mesh slots from 50 to 256 (#2734) +- changed the "enable EIDOS logo" option to disable the Core Design and Bink Video Codec FMVs as well; renamed to "enable legal" (#2741) +- changed sprite pickups to respect the water tint if placed underwater (#2673) +- fixed the bilinear filter to not readjust the UVs (#2258) +- fixed disabling the cutscenes causing the game to exit (#2743, regression from 4.8) +- fixed anisotropy filter causing black lines on certain GPUs (#902) +- fixed mesh faces not being drawn under some circumstances (#2452, #2438) +- fixed objects disappearing too early around screen edges (#2005) +- fixed the trapezoid filter being toggled if Alt-F4 (either left or right) is used to close the game (#2690) +- fixed enemies in one-click high water appearing with a water tint, and not making any animation sounds (#2753) +- fixed the scale of the four keys in St. Francis' Folly (#2652) +- fixed the panther at times not making a sound when it dies, and restored Skate Kid's death SFX (#2647) +- fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776) +- fixed a crash when 3D pickups are disabled and Lara crosses a trigger to look at a pickup item (#2711, regression from 4.8) +- fixed trapezoid filter warping on faces close to the camera (#2629, regression from 4.9) +- fixed Mac builds crashing upon start (regression from 4.9) +- fixed sprites rendering black if no shade value is assigned in the level (#2701, regression from 4.9) +- 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) +- 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) +- fixed the sprite UVs to restore the right and bottom edge pixels (#2672, regression from 4.8) +- fixed sprites missing the fog effect (regression from 4.9) +- fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 4.9) +- fixed wrong PS1-style title bar color for the end of the level stats dialog (regression from 4.9) +- fixed Story So Far showing up even when there's nothing to play (#2611, regression from 2.10) +- fixed Story So Far not playing the opening FMV, `cafe.rpl` (#2779, regression from 2.10) +- fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 3.0) +- fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 3.0) +- improved bubble appearance (#2672) +- improved rendering performance +- improved pause exit dialog - it can now be canceled with escape +- removed the pretty pixels options (it's now always enabled, #2258) ## [4.9](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.3...tr1-4.9) - 2025-03-31 - added quadrilateral interpolation (#354) @@ -52,7 +90,7 @@ ## [4.8.2](https://github.com/LostArtefacts/TRX/compare/tr1-4.8.1...tr1-4.8.2) - 2025-02-15 - changed default FPS value to 60 (#2501) - changed passport to be more responsive to player inputs (#1328) -- fixed story so far not skipping over levels (#2506, regression from 4.8) +- fixed Story So Far not skipping over levels (#2506, regression from 4.8) - fixed resolving paths (especially to music files) on case-sensitive filesystems (#1934, #2504) - improved memory usage by shedding ca. 100-110 MB on average @@ -710,7 +748,7 @@ - added a .NET-based installer - added the option to make Lara revert to pistols on new level start (#557) - added the PS1 style UI (#517) -- added the "Story so far..." option in the select level menu to view cutscenes and FMVs (#201) +- added the "Story So far..." option in the select level menu to view cutscenes and FMVs (#201) ## [2.9.1](https://github.com/LostArtefacts/TRX/compare/tr1-2.9...tr1-2.9.1) - 2022-06-03 - fixed crash on centaur hatch (#579, regression from 2.9) diff --git a/docs/tr1/README.md b/docs/tr1/README.md index cfd199eea..d128fb4b3 100644 --- a/docs/tr1/README.md +++ b/docs/tr1/README.md @@ -1,6 +1,5 @@

-TR1X logo -TR1X logo +TR1X logo

## Windows / Linux @@ -372,6 +371,16 @@ less like this (click to expand): +## Dev snapshots + +To ease the load on our infrastructure, the binary assets such as images and music files are not included in pre-releases and pull request preview builds - they only ship with the full release builds. +However, you can easily download them manually from these urls: + +- https://lostartefacts.dev/aux/tr1x/main.zip (main assets) +- https://lostartefacts.dev/aux/tr1x/music.zip (music files) +- https://lostartefacts.dev/aux/tr1x/trub-music.zip (TR: Unfinished Business expansion pack with fan-patched music) +- https://lostartefacts.dev/aux/tr1x/trub-vanilla.zip (TR: Unfinished Business expansion pack in its original form) + ## Improvements over original game Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for details. @@ -475,6 +484,7 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - fixed a potential softlock when killing the Torso boss in Great Pyramid - fixed being able to shoot the scion multiple times if save/load is used while it blows up - fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames +- fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used #### Cheats - added a fly cheat @@ -607,6 +617,8 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - **Atlantean Stronghold**: converted track 20 in room 4, track 19 in room 13, track 11 in room 17, track 15 in room 20, and track 12 in room 25 to one shot - **The Hive**: converted track 9 in room 8, track 6 in room 18, track 12 in room 30, track 18 in room 31, track 3 in room 32, and track 20 in room 35 to one shot - fixed being unable to load a level that contains no sound effect data +- fixed the panther at times not making a sound when it dies +- restored Skate Kid's death SFX #### Mods - added developer console (accessible with `/`, see [COMMANDS.md](COMMANDS.md) for details) @@ -620,6 +632,7 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - added per-level customizable water color (with customizable blue component) - added per-level customizable fog distance - added deadly water feature from TR2+ +- added support for antitriggers, like TR2+ #### Miscellaneous - added Linux builds @@ -636,6 +649,7 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - expanded maximum texture pages from 32 to 128 - expanded maximum vertices of a single drawable object from 1500 to unlimited - expanded the number of visible enemies from 8 to 32 +- expanded the number of static mesh slots from 50 to 256 - ported audio decoding library to ffmpeg - ported video decoding library to ffmpeg - ported image decoding library to ffmpeg diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 8b8727c06..ca00fd874 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,12 +1,70 @@ -## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...develop) - ××××-××-×× +## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-1.0...develop) - ××××-××-×× + +## [1.0](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...tr2-1.0) - 2025-04-23 - added support for The Golden Mask (#1621) +- added ability to turn off legal screen and FMVs (#2740) +- added ability to turn off ingame cutscenes (#2127) +- added HD images from TR2Main (with Arsunt's consent) +- added sunglasses for graphic options (#1615) +- added control over the fog distances for players and level builders (#1622) +- added control over the water color for players and level builders [see the reference](/docs/GAME_FLOW.md#water-color-table) (#1619) - added an installer for Windows (#2681) - added the bonus level game flow type, which allows for levels to be unlocked if all main game secrets are found (#2668) - added the ability for custom levels to have up to two of each secret type per level (#2674) +- 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) +- added NG+, Japanese, and Japanese NG+ game mode options to the New Game page in the passport (#2731) +- added the ability for spike walls to be reset (antitriggered) +- added the current music track and timestamp to the savegame so they now persist on load (#2579) +- added waterfalls to the savegame so that they now persist on load (#2686) +- added support for aspect ratio-specific images (#1840) +- added a guard to ensure the game always starts on a visible screen even after unplugging displays (#2819) +- changed savegame files to be stored in the `saves` directory (#2087) +- changed the default fog distance to 22 tiles cutting off at 30 tiles to match TR1X (#1622) +- changed the number of static mesh slots from 50 to 256 (#2734) +- changed the maximum number of items (moveables) per level from 256 to 10240 (1024 remains the limit for triggered items) (#1794) +- changed the maximum number of visible enemies from 5 to 32 (#1624) +- changed the maximum number of effects (flames, embers, exploding parts etc) from 100 to 1000 (#1581) +- changed default pitch of the save/load dialog ingame - it's now higher. +- fixed the inability to completely mute the sounds, even at sound volume 0 (#2722) - fixed the final two levels not allowing for secrets to be counted in the statistics (#1582) +- fixed assault course best times not being retained between game relaunches (#1578) +- fixed flares disappearing on the ground when the z buffer is enabled (#1595) - fixed Lara's holsters being empty if a game flow level removes all weapons but also re-adds the pistols (#2677) - fixed the console opening when remapping its key (#2641) +- fixed the boat when it explodes after crossing mines, where Lara's hips would appear rather than exploded boat parts (#1605) +- fixed Lara's hips appearing on Bartoli in the Temple of Xian cutscene (#2558) +- fixed collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground (#2752) +- fixed the lift to work in any cardinal direction in custom levels, not just South (#2100) +- fixed the springboard not responding correctly when Lara drives across one on a skidoo (#1903) +- fixed the drawbridge producing dynamic light when open (#2294) +- fixed the scale of several pickup models in The Golden Mask (#2652) +- fixed the shark in The Cold War not making any sounds when biting Lara (#2678) +- fixed the bird monster not having a shadow (#2060) +- fixed the in-game cinematic camera at times yielding invalid positions (and hence views) in custom levels (#2754) +- fixed a softlock in Temple of Xian if the main chamber key is missed (#2042) +- fixed a potential softlock in Floating Islands if returning towards the level start from the gold secret (#2590) +- fixed a potential softlock in Nightmare in Vegas where the bird monster could remain inactive, or the flip map not set (#1851) +- fixed invalid portals in The Deck between rooms 17 and 104, which could result in Lara seeing enemies in disconnected rooms (#2393) +- fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used (#2776) +- fixed the boat briefly having an underwater hue when Lara first climbs on (#2787) +- fixed destroyed gondolas appearing embedded in the ground after loading a save (#1612) +- fixed a crash in custom levels with large rooms (#2749) +- fixed the viewport not always in sync with the window (#2820) +- fixed inability to move the window to another screen (#2820) +- fixed flares flipped to the right when thrown (regression from 0.10) +- fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 0.10) +- fixed sprites rendering black if no shade value is assigned in the level (#2701, regression from 0.8) +- fixed some 3D pickup items rendering black in software mode (#2792, regression from 0.10) +- fixed Lara at times ending up in incorrect rooms when using the teleport cheat (#2486, regression from 0.3) +- fixed the `/pos` console command reporting the base room number when Lara is actually in a flipped room (#2487, regression from 0.3) +- fixed a crash if an image was missing +- fixed a crash on level load if an animation has no frames (#2746, regression from 0.8) +- fixed flares missing the flicker effect in 60 FPS (#2806, regression from 0.10) +- improved performance when moving the window around +- improved pause exit dialog - it can now be canceled with escape - removed the need to specify in the game flow levels that have no secrets (secrets will be automatically counted) (#1582) +- removed the hard-coded end-level behaviour of the bird guardian for custom levels (#1583) +- removed the FPS and aspect mode options from the config tool (now available in-game in the graphics options) ## [0.10](https://github.com/LostArtefacts/TRX/compare/tr2-0.9.2...tr2-0.10) - 2025-03-18 - added support for 60 FPS rendering @@ -298,6 +356,7 @@ - fixed harpoon bolts damaging inactive enemies (#1804) - fixed enemies that are run over by the skidoo not being counted in the statistics (#1772) - fixed sound settings resuming the music (#1707) +- fixed being able to use hotkeys in the end-level statistics screen - fixed the inventory ring spinout animation sometimes running too fast (#1704, regression from 0.3) - fixed new saves not displaying the save count in the passport (#1591) - fixed certain erroneous `/play` invocations resulting in duplicated error messages diff --git a/docs/tr2/README.md b/docs/tr2/README.md index 7982c6cdd..9faec4fd4 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -1,6 +1,5 @@

-TR2X logo -TR2X logo +TR2X logo

TR2X is finished with the decompilation and is now able to run without the @@ -157,12 +156,22 @@ as Notepad. 4. Find TR2X in your Applications folder. Right-click it and click "Show Package Contents". 5. Copy your Tomb Raider 2 game data files into `Contents/Resources`. +## Dev snapshots + +To ease the load on our infrastructure, the binary assets such as images and music files are not included in pre-releases and pull request preview builds - they only ship with the full release builds. +However, you can easily download them manually from these urls: + +- https://lostartefacts.dev/aux/tr2x/main.zip (main assets) +- https://lostartefacts.dev/aux/tr2x/music.zip (music files) +- https://lostartefacts.dev/aux/tr2x/trgm.zip (TR: The Golden Mask expansion pack) + ## Improvements over original game #### UI - added support for more accented characters - added fade effects to displayed images - added a wireframe mode +- added sunglasses for graphic options - improved support for windowed mode #### Gameplay @@ -178,8 +187,11 @@ as Notepad. - added a pause screen - added a photo mode feature - added combined support for The Golden Mask +- added NG+, Japanese, and Japanese NG+ game mode options to the New Game page in the passport +- added waterfalls to the savegame so that they now persist on load - changed inventory to pause the music rather than muting it - fixed killing the T-Rex with a grenade launcher crashing the game +- fixed assault course best times not being retained between game relaunches - fixed secret rewards not displaying shotgun ammo - fixed numeric keys interfering with the demos - fixed the ammo counter being hidden while a demo plays in NG+ @@ -209,14 +221,17 @@ as Notepad. - fixed the following floor data issues: - **Opera House**: fixed the trigger under item 203 to trigger it rather than item 204 - **Wreck of the Maria Doria**: fixed room 98 not having water + - **The Deck**: fixed invalid portals between rooms 17 and 104, which could result in Lara seeing enemies in disconnected rooms - **Tibetan Foothills**: added missing triggers for the drawbridge in room 96 (after the flipmap) - **Catacombs of the Talion**: changed some music triggers to pads near the first yeti, and added missing triggers and ladder in room 116 (after the flipmap) - **Ice Palace**: fixed door 143's position to resolve the invisible wall in front of it, and added an extra pickup trigger beside the Gong Hammer in room 29 - - **Temple of Xian**: fixed missing death tiles in room 91 - - **Floating Islands**: fixed door 72's position to resolve the invisible wall in front of it + - **Temple of Xian**: fixed missing death tiles in room 91; adding trigger workarounds to avoid a softlock after (missing) the final key + - **Floating Islands**: fixed door 72's position to resolve the invisible wall in front of it; added extra zipline reset triggers to avoid softlock + - **Nightmare in Vegas**: added additional triggers for the bird monster and final flip map to avoid softlock - fixed the game crashing if a cinematic is triggered but the level contains no cinematic frames - fixed smashed windows blocking enemy pathing after loading a save - fixed Lara getting stuck in a T-pose after jumping/falling and then dying before reaching fast fall speed +- fixed collision issues with drawbridges, trapdoors, and bridges when stacked over each other, over slopes, and near the ground - fixed several issues with pushblocks: - fixed an invisible wall above stacked pushblocks if near a ceiling portal - fixed floor height issues with pushblocks poised to fall in various scenarios @@ -237,6 +252,9 @@ as Notepad. - increased Barkhang Monastery rooftops key size - increased Temple of Xian dragon seal size, and fixed inventory rotation - fixed Floating Islands mystic plaque inventory rotation +- fixed pushblocks being rotated when Lara grabs them, most noticeable if asymmetric textures have been used +- fixed being able to use hotkeys in the end-level statistics screen +- fixed destroyed gondolas appearing embedded in the ground after loading a save - improved the animation of Lara's braid #### Cheats @@ -282,17 +300,23 @@ as Notepad. - fixed TGA screenshots crashing the game - fixed the camera being cut off after using the gong hammer in Ice Palace - fixed Lara's underwater hue being retained when re-entering a boat +- fixed the boat briefly having an underwater hue when Lara first climbs on - fixed distant rooms sometimes not appearing, causing the skybox to be visible when it shouldn't - fixed rendering problems on certain Intel GPUs - fixed bubbles spawning from flares if Lara is in shallow water - fixed the inventory up arrow at times overlapping the health bar - fixed blood spawning on Lara from gunshots using incorrect positioning data +- fixed the drawbridge producing dynamic light when open +- fixed the boat when it explodes after crossing mines, where Lara's hips would appear rather than exploded boat parts +- fixed Lara's hips appearing on Bartoli in the Temple of Xian cutscene +- fixed the bird monster not having a shadow - improved FMV mode behavior - stopped switching screen resolutions - improved vertex movement when looking through water portals - improved support for non-4:3 aspect ratios #### Audio - added an option to control how music is played while underwater rather than simply muting it +- added the current music track and timestamp to the savegame so they now persist on load - fixed music not playing with certain game versions - fixed the audio not being in sync when Lara strikes the gong in Ice Palace - fixed sound settings resuming the music @@ -305,11 +329,17 @@ as Notepad. #### Mods - added developer console (accessible with `/`, see [COMMANDS.md](COMMANDS.md) for details) - added ability to disable FMVs +- added per-level customizable fog distance +- added the ability for spike walls to be reset (antitriggered) +- fixed the lift to work in any cardinal direction in custom levels, not just South +- fixed the springboard not responding correctly when Lara drives across one on a skidoo +- removed the hard-coded end-level behaviour of the bird guardian for custom levels #### Miscellaneous - added Linux builds - added macOS builds - added .jpeg/.png screenshots +- 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 - added ability to skip FMVs with both the Action key - added ability to skip end credits with the Action and Escape keys - added the ability to specify per-level SFX files rather than enforcing the default (main.sfx) on all levels @@ -319,6 +349,10 @@ as Notepad. - expanded maximum object textures from 2048 to unlimited (within game's overall memory cap) - expanded maximum sprite textures from 512 to unlimited (within game's overall memory cap) - expanded maximum texture pages from 32 to 128 +- expanded the number of static mesh slots from 50 to 256 +- expanded maximum number of items (moveables) from 256 to 10240 (1024 remains the limit for triggered items) +- expanded maximum number of visible enemies from 5 to 32 +- expanded the maximum number of effects (flames, embers, exploding parts etc) from 100 to 1000 - ported audio decoding library to ffmpeg - ported video decoding library to ffmpeg - ported input backend to SDL diff --git a/justfile b/justfile index 007f4dede..ec8069c42 100644 --- a/justfile +++ b/justfile @@ -49,6 +49,9 @@ push-image-win: (image-win "0") (_docker_push "rrdash/trx-win") import "justfile.tr1" import "justfile.tr2" +download-assets tr_version='all': + tools/download_assets {{tr_version}} + output-release-name tr_version: tools/output_release_name {{tr_version}} diff --git a/justfile.tr2 b/justfile.tr2 index a7907cba2..6b28dff3f 100644 --- a/justfile.tr2 +++ b/justfile.tr2 @@ -20,6 +20,6 @@ tr2-package-win-installer target='release': \ (_docker_run "rrdash/trx-win" "package" "--tr-version" "2" "-o" "tools/installer/TR2X_Installer/Resources/release.zip") (tr2-build-win-installer) #!/bin/sh git checkout "tools/installer/TR2X_Installer/Resources/release.zip" - exe_name=TR2X-$(tools/get_version 1)-Installer.exe + exe_name=TR2X-$(tools/get_version 2)-Installer.exe cp tools/installer/out/TR2X_Installer.exe "${exe_name}" echo "Created ${exe_name}" diff --git a/src/libtrx/config/file.c b/src/libtrx/config/file.c index c9fc5c3d9..29a83e785 100644 --- a/src/libtrx/config/file.c +++ b/src/libtrx/config/file.c @@ -1,5 +1,6 @@ #include "config/file.h" +#include "colors.h" #include "debug.h" #include "filesystem.h" #include "game/console/history.h" @@ -7,6 +8,7 @@ #include "memory.h" #include "strings.h" +#include #include #define EMPTY_ROOT "{}" @@ -244,6 +246,32 @@ void ConfigFile_LoadOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options) *(int *)opt->target = ConfigFile_ReadEnum( root_obj, M_ResolveOptionName(opt->name), *(int *)opt->default_value, opt->param); + break; + + case COT_RGB888: { + RGB_888 *const target = (RGB_888 *)opt->target; + JSON_VALUE *const value = + JSON_ObjectGetValue(root_obj, M_ResolveOptionName(opt->name)); + bool success = false; + if (value != nullptr && value->type == JSON_TYPE_NUMBER) { + const uint32_t rgb_value = + JSON_ValueGetInt(value, JSON_INVALID_NUMBER); + ASSERT(rgb_value != JSON_INVALID_NUMBER); + target->r = (rgb_value >> 0) & 0xFF; + target->g = (rgb_value >> 8) & 0xFF; + target->b = (rgb_value >> 16) & 0xFF; + success = true; + } else if (value != nullptr && value->type == JSON_TYPE_STRING) { + const char *str_value = + JSON_ValueGetString(value, JSON_INVALID_STRING); + ASSERT(str_value != JSON_INVALID_STRING); + success = String_ParseRGB888(str_value, target); + } + if (!success) { + *(RGB_888 *)opt->target = *(RGB_888 *)opt->default_value; + } + break; + } } opt++; } @@ -282,6 +310,15 @@ void ConfigFile_DumpOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options) root_obj, M_ResolveOptionName(opt->name), *(int *)opt->target, (const char *)opt->param); break; + + case COT_RGB888: { + const RGB_888 *const color = (RGB_888 *)opt->target; + char tmp[10]; + sprintf(tmp, "#%02X%02X%02X", color->r, color->g, color->b); + JSON_ObjectAppendString( + root_obj, M_ResolveOptionName(opt->name), tmp); + break; + } } opt++; } @@ -303,3 +340,49 @@ void ConfigFile_WriteEnum( { JSON_ObjectAppendString(obj, name, EnumMap_ToString(enum_name, value)); } + +bool ConfigFile_LoadAssaultStats( + JSON_OBJECT *const root_obj, ASSAULT_STATS *const assault_stats) +{ + JSON_OBJECT *const stats_obj = + JSON_ObjectGetObject(root_obj, "assault_stats"); + if (stats_obj == nullptr) { + return false; + } + JSON_ARRAY *const entries_arr = JSON_ObjectGetArray(stats_obj, "entries"); + if (entries_arr != nullptr) { + for (size_t i = 0; i < entries_arr->length && i < MAX_ASSAULT_TIMES; + i++) { + JSON_OBJECT *const entry_obj = JSON_ArrayGetObject(entries_arr, i); + if (entry_obj != nullptr) { + assault_stats->entries[i].time = JSON_ObjectGetInt( + entry_obj, "time", assault_stats->entries[i].time); + assault_stats->entries[i].attempt_num = JSON_ObjectGetInt( + entry_obj, "attempt_num", + assault_stats->entries[i].attempt_num); + } + } + } + assault_stats->total_attempts = JSON_ObjectGetInt( + stats_obj, "total_attempts", assault_stats->total_attempts); + return true; +} + +bool ConfigFile_DumpAssaultStats( + JSON_OBJECT *const root_obj, const ASSAULT_STATS *const assault_stats) +{ + JSON_OBJECT *const stats_obj = JSON_ObjectNew(); + JSON_ARRAY *const entries_arr = JSON_ArrayNew(); + for (int i = 0; i < MAX_ASSAULT_TIMES; i++) { + JSON_OBJECT *const entry_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(entry_obj, "time", assault_stats->entries[i].time); + JSON_ObjectAppendInt( + entry_obj, "attempt_num", assault_stats->entries[i].attempt_num); + JSON_ArrayAppendObject(entries_arr, entry_obj); + } + JSON_ObjectAppendArray(stats_obj, "entries", entries_arr); + JSON_ObjectAppendInt( + stats_obj, "total_attempts", assault_stats->total_attempts); + JSON_ObjectAppendObject(root_obj, "assault_stats", stats_obj); + return true; +} diff --git a/src/libtrx/config/file.h b/src/libtrx/config/file.h index 7da1e873f..b6f0819ea 100644 --- a/src/libtrx/config/file.h +++ b/src/libtrx/config/file.h @@ -2,6 +2,7 @@ #include "config/option.h" #include "enum_map.h" +#include "game/gym.h" #include "json.h" #include @@ -25,3 +26,8 @@ int ConfigFile_ReadEnum( const char *enum_name); void ConfigFile_WriteEnum( JSON_OBJECT *obj, const char *name, int value, const char *enum_name); + +bool ConfigFile_LoadAssaultStats( + JSON_OBJECT *root_obj, ASSAULT_STATS *assault_stats); +bool ConfigFile_DumpAssaultStats( + JSON_OBJECT *root_obj, const ASSAULT_STATS *assault_stats); diff --git a/src/libtrx/config/map.c b/src/libtrx/config/map.c index 25c1c9e16..ca43ff0d6 100644 --- a/src/libtrx/config/map.c +++ b/src/libtrx/config/map.c @@ -1,3 +1,4 @@ +#include "colors.h" #include "config/option.h" #include "config/types.h" #include "config/vars.h" @@ -39,6 +40,13 @@ .default_value = &(int32_t) { default_value_ }, \ .param = ENUM_MAP_NAME(enum_map) }, +#define CFG_RGB888(parent, target_, default_r, default_g, default_b) \ + { .name = QUOTE(target_), \ + .type = COT_RGB888, \ + .target = &parent.target_, \ + .default_value = &(RGB_888) { default_r, default_g, default_b }, \ + .param = nullptr }, + static const CONFIG_OPTION m_ConfigOptionMap[] = { #include "map.def" {}, // sentinel diff --git a/src/libtrx/config/map_tr1.def b/src/libtrx/config/map_tr1.def index 3a4504ad6..0d3219eeb 100644 --- a/src/libtrx/config/map_tr1.def +++ b/src/libtrx/config/map_tr1.def @@ -6,6 +6,9 @@ CFG_BOOL(g_Config, gameplay.disable_shotgun, false) CFG_ENUM(g_Config, gameplay.stat_detail_mode, SDM_FULL, STAT_DETAIL_MODE) CFG_BOOL(g_Config, gameplay.enable_deaths_counter, true) CFG_BOOL(g_Config, gameplay.enable_enhanced_look, true) +CFG_INT32(g_Config, visuals.fog_start, 22) +CFG_INT32(g_Config, visuals.fog_end, 30) +CFG_RGB888(g_Config, visuals.water_color, 0x72, 0xFF, 0xFF) CFG_BOOL(g_Config, visuals.enable_gun_lighting, true) CFG_BOOL(g_Config, visuals.enable_shotgun_flash, true) CFG_BOOL(g_Config, gameplay.fix_shotgun_targeting, true) @@ -39,9 +42,9 @@ CFG_INT32(g_Config, rendering.resolution_width, -1) CFG_INT32(g_Config, rendering.resolution_height, -1) CFG_BOOL(g_Config, gameplay.enable_demo, true) CFG_BOOL(g_Config, gameplay.enable_fmv, true) -CFG_BOOL(g_Config, gameplay.enable_eidos_logo, true) +CFG_BOOL(g_Config, gameplay.enable_legal, true) CFG_BOOL(g_Config, gameplay.enable_loading_screens, false) -CFG_BOOL(g_Config, gameplay.enable_cine, true) +CFG_BOOL(g_Config, gameplay.enable_cutscenes, true) CFG_BOOL(g_Config, audio.enable_music_in_menu, true) CFG_BOOL(g_Config, audio.enable_music_in_inventory, true) CFG_ENUM(g_Config, audio.underwater_music_mode, UMM_FULL, UNDERWATER_MUSIC_MODE) @@ -105,7 +108,6 @@ CFG_BOOL(g_Config, rendering.enable_wireframe, false) CFG_DOUBLE(g_Config, rendering.wireframe_width, 2.5) CFG_BOOL(g_Config, rendering.enable_trapezoid_filter, true) CFG_BOOL(g_Config, rendering.enable_vsync, true) -CFG_BOOL(g_Config, rendering.pretty_pixels, true) CFG_BOOL(g_Config, visuals.enable_reflections, true) CFG_INT32(g_Config, audio.music_volume, 8) CFG_INT32(g_Config, audio.sound_volume, 8) @@ -120,6 +122,7 @@ CFG_BOOL(g_Config, visuals.enable_skybox, true) CFG_BOOL(g_Config, visuals.enable_ps1_crystals, true) CFG_BOOL(g_Config, ui.enable_game_ui, true) CFG_BOOL(g_Config, ui.enable_photo_mode_ui, true) +CFG_BOOL(g_Config, ui.enable_wraparound, true) CFG_BOOL(g_Config, gameplay.enable_item_examining, true) CFG_BOOL(g_Config, gameplay.enable_auto_item_selection, true) CFG_BOOL(g_Config, gameplay.enable_pickup_aids, false) diff --git a/src/libtrx/config/map_tr2.def b/src/libtrx/config/map_tr2.def index b154147c1..bdb48896f 100644 --- a/src/libtrx/config/map_tr2.def +++ b/src/libtrx/config/map_tr2.def @@ -8,9 +8,12 @@ CFG_BOOL(g_Config, gameplay.fix_floor_data_issues, true) CFG_BOOL(g_Config, gameplay.fix_flare_throw_priority, true) CFG_BOOL(g_Config, gameplay.fix_walk_run_jump, true) CFG_BOOL(g_Config, gameplay.fix_bear_ai, true) +CFG_BOOL(g_Config, gameplay.fix_bridge_collision, true) CFG_BOOL(g_Config, gameplay.enable_cheats, false) CFG_BOOL(g_Config, gameplay.enable_console, true) CFG_BOOL(g_Config, gameplay.enable_fmv, true) +CFG_BOOL(g_Config, gameplay.enable_legal, true) +CFG_BOOL(g_Config, gameplay.enable_cutscenes, true) CFG_BOOL(g_Config, input.enable_tr3_sidesteps, true) CFG_BOOL(g_Config, input.enable_responsive_passport, true) CFG_BOOL(g_Config, gameplay.enable_auto_item_selection, true) @@ -20,11 +23,16 @@ CFG_BOOL(g_Config, visuals.enable_gun_lighting, true) CFG_BOOL(g_Config, visuals.enable_fade_effects, true) CFG_BOOL(g_Config, visuals.enable_exit_fade_effects, true) CFG_BOOL(g_Config, visuals.fix_item_rots, true) +CFG_BOOL(g_Config, visuals.fix_texture_issues, true) CFG_INT32(g_Config, visuals.fov, 80) -CFG_BOOL(g_Config, visuals.use_pcx_fov, true) +CFG_BOOL(g_Config, visuals.use_psx_fov, true) +CFG_INT32(g_Config, visuals.fog_start, 22) +CFG_INT32(g_Config, visuals.fog_end, 30) +CFG_RGB888(g_Config, visuals.water_color, 0x80, 0xDF, 0xFF) CFG_DOUBLE(g_Config, ui.text_scale, 1.0) CFG_DOUBLE(g_Config, ui.bar_scale, 1.0) CFG_BOOL(g_Config, ui.enable_photo_mode_ui, true) +CFG_BOOL(g_Config, ui.enable_wraparound, true) CFG_INT32(g_Config, rendering.fps, 60) CFG_ENUM(g_Config, rendering.screenshot_format, SCREENSHOT_FORMAT_JPEG, SCREENSHOT_FORMAT) CFG_ENUM(g_Config, rendering.render_mode, RM_HARDWARE, RENDER_MODE) @@ -53,4 +61,6 @@ CFG_INT32(g_Config, audio.sound_volume, 10) CFG_INT32(g_Config, audio.music_volume, 10) CFG_BOOL(g_Config, audio.enable_lara_mic, false) CFG_ENUM(g_Config, audio.underwater_music_mode, UMM_FULL, UNDERWATER_MUSIC_MODE) -CFG_BOOL(g_Config, profile.bonus_level_unlock, false) +CFG_ENUM(g_Config, audio.music_load_condition, MUSIC_LOAD_NON_AMBIENT, MUSIC_LOAD_CONDITION) +CFG_BOOL(g_Config, gameplay.enable_game_modes, true) +CFG_BOOL(g_Config, profile.new_game_plus_unlock, false) diff --git a/src/libtrx/config/priv_tr1.c b/src/libtrx/config/priv_tr1.c index db4833581..34b6eac6f 100644 --- a/src/libtrx/config/priv_tr1.c +++ b/src/libtrx/config/priv_tr1.c @@ -117,6 +117,10 @@ static void M_LoadLegacyOptions(JSON_OBJECT *const parent_obj) READ_FALLBACK_INT(g_Config.window.height, "window_height"); READ_FALLBACK_INT(g_Config.input.keyboard_layout, "layout"); READ_FALLBACK_INT(g_Config.input.controller_layout, "cntlr_layout"); + + // ..4.9 + READ_FALLBACK_BOOL(g_Config.gameplay.enable_cutscenes, "enable_cine"); + READ_FALLBACK_BOOL(g_Config.gameplay.enable_legal, "enable_eidos_logo"); } static void M_DumpKeyboardLayout( @@ -174,6 +178,9 @@ static void M_DumpControllerLayout( void Config_LoadFromJSON(JSON_OBJECT *root_obj) { ConfigFile_LoadOptions(root_obj, Config_GetOptionMap()); + if (Gym_HasAssaultStats()) { + ConfigFile_LoadAssaultStats(root_obj, &g_Config.profile.assault_stats); + } for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { @@ -182,13 +189,15 @@ void Config_LoadFromJSON(JSON_OBJECT *root_obj) } M_LoadLegacyOptions(root_obj); - g_Config.loaded = true; } void Config_DumpToJSON(JSON_OBJECT *root_obj) { ConfigFile_DumpOptions(root_obj, Config_GetOptionMap()); + if (Gym_HasAssaultStats()) { + ConfigFile_DumpAssaultStats(root_obj, &g_Config.profile.assault_stats); + } for (INPUT_LAYOUT layout = INPUT_LAYOUT_CUSTOM_1; layout < INPUT_LAYOUT_NUMBER_OF; layout++) { @@ -204,6 +213,8 @@ void Config_DumpToJSON(JSON_OBJECT *root_obj) void Config_Sanitize(void) { CLAMP(g_Config.gameplay.start_lara_hitpoints, 1, LARA_MAX_HITPOINTS); + CLAMP(g_Config.visuals.fog_start, 1, 100); + CLAMP(g_Config.visuals.fog_end, 1, 100); CLAMP(g_Config.visuals.fov_value, 30, 150); CLAMP(g_Config.gameplay.camera_speed, 1, 10); CLAMP(g_Config.audio.music_volume, 0, 10); diff --git a/src/libtrx/config/priv_tr2.c b/src/libtrx/config/priv_tr2.c index 19953ccaf..c5c9eea96 100644 --- a/src/libtrx/config/priv_tr2.c +++ b/src/libtrx/config/priv_tr2.c @@ -4,6 +4,7 @@ #include "config/vars.h" #include "debug.h" #include "game/clock.h" +#include "game/gym.h" #include "game/input.h" #include "log.h" #include "utils.h" @@ -16,6 +17,7 @@ static void M_LoadInputLayout( static void M_DumpInputConfig(JSON_OBJECT *root_obj); static void M_DumpInputLayout( JSON_OBJECT *parent_obj, INPUT_BACKEND backend, INPUT_LAYOUT layout); +static void M_LoadLegacyOptions(JSON_OBJECT *const parent_obj); static void M_LoadInputConfig(JSON_OBJECT *const root_obj) { @@ -98,10 +100,23 @@ static void M_DumpInputLayout( } } +static void M_LoadLegacyOptions(JSON_OBJECT *const parent_obj) +{ +#define READ_FALLBACK_BOOL(target, key) \ + target = JSON_ObjectGetBool(parent_obj, key, target) + + // ..0.10 + READ_FALLBACK_BOOL(g_Config.visuals.use_psx_fov, "use_pcx_fov"); +} + void Config_LoadFromJSON(JSON_OBJECT *root_obj) { ConfigFile_LoadOptions(root_obj, Config_GetOptionMap()); + if (Gym_HasAssaultStats()) { + ConfigFile_LoadAssaultStats(root_obj, &g_Config.profile.assault_stats); + } M_LoadInputConfig(root_obj); + M_LoadLegacyOptions(root_obj); g_Config.loaded = true; g_SavedConfig = g_Config; } @@ -109,6 +124,9 @@ void Config_LoadFromJSON(JSON_OBJECT *root_obj) void Config_DumpToJSON(JSON_OBJECT *root_obj) { ConfigFile_DumpOptions(root_obj, Config_GetOptionMap()); + if (Gym_HasAssaultStats()) { + ConfigFile_DumpAssaultStats(root_obj, &g_Config.profile.assault_stats); + } M_DumpInputConfig(root_obj); } @@ -137,6 +155,8 @@ void Config_Sanitize(void) g_Config.rendering.fps = 30; } + CLAMP(g_Config.visuals.fog_start, 1, 100); + CLAMP(g_Config.visuals.fog_end, 1, 100); CLAMP(g_Config.visuals.fov, 30, 150); CLAMP(g_Config.ui.bar_scale, 0.5, 2.0); CLAMP(g_Config.ui.text_scale, 0.5, 2.0); diff --git a/src/libtrx/engine/image.c b/src/libtrx/engine/image.c index 9449a1a8c..f209f8627 100644 --- a/src/libtrx/engine/image.c +++ b/src/libtrx/engine/image.c @@ -59,7 +59,12 @@ static bool M_Init(const char *const path, IMAGE_READER_CONTEXT *const ctx) ctx->packet = nullptr; char *full_path = File_GetFullPath(path); - int32_t error_code = + int32_t error_code = 0; + if (full_path == nullptr) { + error_code = AVERROR(ENOENT); + goto finish; + } + error_code = avformat_open_input(&ctx->format_ctx, full_path, nullptr, nullptr); Memory_FreePointer(&full_path); diff --git a/src/libtrx/game/anims/common.c b/src/libtrx/game/anims/common.c index 042d8c187..10a673c65 100644 --- a/src/libtrx/game/anims/common.c +++ b/src/libtrx/game/anims/common.c @@ -7,6 +7,7 @@ static ANIM *m_Anims = nullptr; static ANIM_CHANGE *m_Changes = nullptr; static ANIM_RANGE *m_Ranges = nullptr; static ANIM_BONE *m_Bones = nullptr; +static ANIM m_NullAnim = {}; void Anim_InitialiseAnims(const int32_t num_anims) { @@ -37,7 +38,7 @@ int32_t Anim_GetTotalCount(void) ANIM *Anim_GetAnim(const int32_t anim_idx) { - return &m_Anims[anim_idx]; + return anim_idx == NO_ANIM ? &m_NullAnim : &m_Anims[anim_idx]; } ANIM_CHANGE *Anim_GetChange(const int32_t change_idx) diff --git a/src/libtrx/game/anims/frames.c b/src/libtrx/game/anims/frames.c index fbb67706c..f7aa6b45c 100644 --- a/src/libtrx/game/anims/frames.c +++ b/src/libtrx/game/anims/frames.c @@ -41,6 +41,10 @@ static int32_t M_GetAnimFrameCount( uint32_t next_ofs = anim_idx == Anim_GetTotalCount() - 1 ? (unsigned)(sizeof(int16_t) * frame_data_length) : Anim_GetAnim(anim_idx + 1)->frame_ofs; + if (anim->frame_size == 0) { + ASSERT(next_ofs - anim->frame_ofs == 0); + return 0; + } return (next_ofs - anim->frame_ofs) / (int32_t)(sizeof(int16_t) * anim->frame_size); #endif @@ -201,7 +205,7 @@ void Anim_LoadFrames(const int16_t *data, const int32_t data_length) // so ensure everything that's loaded is configured as such. for (int32_t i = 0; i < O_NUMBER_OF; i++) { OBJECT *const obj = Object_Get(i); - if (obj->loaded && obj->mesh_count >= 0 && obj->anim_idx == -1 + if (obj->loaded && obj->mesh_count >= 0 && obj->anim_idx == NO_ANIM && obj->frame_base == nullptr) { obj->frame_base = M_FindFrameBase(obj->frame_ofs); } diff --git a/src/libtrx/game/camera/common.c b/src/libtrx/game/camera/common.c index 798388ccc..700c77d96 100644 --- a/src/libtrx/game/camera/common.c +++ b/src/libtrx/game/camera/common.c @@ -50,8 +50,10 @@ void Camera_ClampInterpResult(void) CLAMP(pos->z, box->left, box->right); finish: - const int32_t floor = Room_GetHeight(sector, pos->x, pos->y, pos->z); - const int32_t ceiling = Room_GetCeiling(sector, pos->x, pos->y, pos->z); + const int32_t floor = + Room_GetHeightEx(sector, pos->x, pos->y, pos->z, true); + const int32_t ceiling = + Room_GetCeilingEx(sector, pos->x, pos->y, pos->z, true); if (floor != NO_HEIGHT && ceiling != NO_HEIGHT) { CLAMP(pos->y, ceiling - shift, floor - shift); } diff --git a/src/libtrx/game/collision.c b/src/libtrx/game/collision.c index 701bffbf1..e459289a3 100644 --- a/src/libtrx/game/collision.c +++ b/src/libtrx/game/collision.c @@ -14,12 +14,8 @@ static bool M_IsOnWalkable( const SECTOR *const sector, const int32_t x, const int32_t y, const int32_t z, const int32_t room_height) { -#if TR_VERSION == 1 return g_Config.gameplay.fix_bridge_collision && Room_IsOnWalkable(sector, x, y, z, room_height); -#elif TR_VERSION >= 2 - return false; -#endif } int32_t Collide_GetSpheres( @@ -69,18 +65,7 @@ int32_t Collide_GetSpheres( Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame->mesh_rots[i]); - - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); mesh = Object_GetMesh(obj->mesh_idx + i); Matrix_Push(); @@ -158,18 +143,7 @@ void Collide_GetJointAbsPosition( Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame->mesh_rots[i + 1]); - - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); } Matrix_TranslateRel32(*out_vec); @@ -291,11 +265,7 @@ void Collide_GetCollisionInfo( coll->side_front.ceiling = ceiling; coll->side_front.type = Room_GetHeightType(); -#if TR_VERSION == 1 is_on_walkable = M_IsOnWalkable(sector, x, y_top, z, room_height); -#elif TR_VERSION >= 2 - is_on_walkable = false; -#endif if (!is_on_walkable) { if (coll->slopes_are_walls && coll->side_front.type == HT_BIG_SLOPE && coll->side_front.floor < 0) { diff --git a/src/libtrx/game/console/cmd/config.c b/src/libtrx/game/console/cmd/config.c index 2979bfe95..4eccfe895 100644 --- a/src/libtrx/game/console/cmd/config.c +++ b/src/libtrx/game/console/cmd/config.c @@ -1,5 +1,6 @@ #include "game/console/cmd/config.h" +#include "colors.h" #include "config.h" #include "debug.h" #include "enum_map.h" @@ -116,6 +117,13 @@ bool Console_Cmd_Config_GetCurrentValue( target, target_size, "%s", EnumMap_ToString(option->param, *(int32_t *)option->target)); break; + case COT_RGB888: { + const RGB_888 *color = option->target; + snprintf( + target, target_size, "%02hhx%02hhx%02hhx", color->r, color->g, + color->b); + break; + } } return true; } @@ -175,6 +183,18 @@ bool Console_Cmd_Config_SetCurrentValue( } break; } + + case COT_RGB888: { + uint8_t r, g, b; + if (sscanf(new_value, "%02hhx%02hhx%02hhx", &r, &g, &b) == 3) { + RGB_888 *const color = (RGB_888 *)option->target; + color->r = r; + color->g = g; + color->b = b; + return true; + } + break; + } } return false; diff --git a/src/libtrx/game/console/cmd/pos.c b/src/libtrx/game/console/cmd/pos.c index 34cfbb2fd..0f150fbc5 100644 --- a/src/libtrx/game/console/cmd/pos.c +++ b/src/libtrx/game/console/cmd/pos.c @@ -6,6 +6,7 @@ #include "game/game_string.h" #include "game/lara/common.h" #include "game/objects/common.h" +#include "game/rooms.h" #include "memory.h" #include "strings.h" @@ -45,10 +46,15 @@ static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx) String_Format(level_type_fmt, current_level->num + reindex); const ITEM *const lara_item = Lara_GetItem(); + int16_t room_num = lara_item->room_num; + const ROOM *const room = Room_Get(room_num); + if (Room_GetFlipStatus() && room->flipped_room != NO_ROOM_NEG) { + room_num = room->flipped_room; + } char *details = lara_item == nullptr ? String_Format("%s", GS(OSD_POS_LARA_MISSING)) : String_Format( - GS(OSD_POS_LARA_POS_FMT), lara_item->room_num, + GS(OSD_POS_LARA_POS_FMT), room_num, lara_item->pos.x / (float)WALL_L, lara_item->pos.y / (float)WALL_L, lara_item->pos.z / (float)WALL_L, diff --git a/src/libtrx/game/console/cmd/teleport.c b/src/libtrx/game/console/cmd/teleport.c index 02dbe97b8..d8479f7b3 100644 --- a/src/libtrx/game/console/cmd/teleport.c +++ b/src/libtrx/game/console/cmd/teleport.c @@ -156,7 +156,7 @@ static COMMAND_RESULT M_TeleportToXYZ(float x, const float y, float z) z += 0.5f; } - if (!Lara_Cheat_Teleport(x * WALL_L, y * WALL_L, z * WALL_L)) { + if (!Lara_Cheat_Teleport(x * WALL_L, y * WALL_L, z * WALL_L, NO_ROOM)) { Console_Log(GS(OSD_POS_SET_POS_FAIL), x, y, z); return CR_FAILURE; } @@ -185,7 +185,7 @@ static COMMAND_RESULT M_TeleportToRoom(const int16_t room_num) int32_t x = x1 + Random_GetControl() * (x2 - x1) / 0x7FFF; int32_t y = y1; int32_t z = z1 + Random_GetControl() * (z2 - z1) / 0x7FFF; - if (Lara_Cheat_Teleport(x, y, z)) { + if (Lara_Cheat_Teleport(x, y, z, room_num)) { success = true; break; } @@ -219,8 +219,8 @@ static COMMAND_RESULT M_TeleportToObject(const char *const user_input) } if (Lara_Cheat_Teleport( - best_item->pos.x, best_item->pos.y - STEP_L / 4, - best_item->pos.z)) { + best_item->pos.x, best_item->pos.y - STEP_L / 4, best_item->pos.z, + best_item->room_num)) { Console_Log(GS(OSD_POS_SET_ITEM), obj_name); } else { Console_Log(GS(OSD_POS_SET_ITEM_FAIL), obj_name); diff --git a/src/libtrx/game/console/common.c b/src/libtrx/game/console/common.c index e3b6052a0..70454aa7a 100644 --- a/src/libtrx/game/console/common.c +++ b/src/libtrx/game/console/common.c @@ -4,7 +4,7 @@ #include "debug.h" #include "game/console/registry.h" #include "game/game_string.h" -#include "game/ui/widgets/console.h" +#include "game/ui.h" #include "log.h" #include "memory.h" #include "strings.h" @@ -14,20 +14,18 @@ #include static bool m_IsOpened = false; -static UI_WIDGET *m_Console; +static UI_CONSOLE_STATE m_UIState = {}; void Console_Init(void) { - m_Console = UI_Console_Create(); + UI_Console_Init(&m_UIState); + Console_History_Init(); } void Console_Shutdown(void) { - if (m_Console != nullptr) { - m_Console->free(m_Console); - m_Console = nullptr; - } + UI_Console_Free(&m_UIState); Console_History_Shutdown(); Console_Registry_Shutdown(); @@ -38,16 +36,21 @@ void Console_Shutdown(void) void Console_Open(void) { if (m_IsOpened) { - UI_Console_HandleClose(m_Console); + return; } m_IsOpened = true; - UI_Console_HandleOpen(m_Console); + UI_FireEvent( + (EVENT) { .name = "console_open", .sender = nullptr, .data = nullptr }); } void Console_Close(void) { - UI_Console_HandleClose(m_Console); + if (!m_IsOpened) { + return; + } m_IsOpened = false; + UI_FireEvent((EVENT) { + .name = "console_close", .sender = nullptr, .data = nullptr }); } bool Console_IsOpened(void) @@ -55,21 +58,6 @@ bool Console_IsOpened(void) return m_IsOpened; } -void Console_ScrollLogs(void) -{ - UI_Console_ScrollLogs(m_Console); -} - -int32_t Console_GetVisibleLogCount(void) -{ - return UI_Console_GetVisibleLogCount(m_Console); -} - -int32_t Console_GetMaxLogCount(void) -{ - return UI_Console_GetMaxLogCount(m_Console); -} - void Console_Log(const char *fmt, ...) { ASSERT(fmt != nullptr); @@ -86,7 +74,12 @@ void Console_Log(const char *fmt, ...) va_end(va); LOG_INFO("%s", text); - UI_Console_HandleLog(m_Console, text); + + UI_FireEvent((EVENT) { + .name = "console_log", + .sender = nullptr, + .data = text, + }); Memory_FreePointer(&text); } @@ -135,17 +128,12 @@ COMMAND_RESULT Console_Eval(const char *const cmdline) return result; } +void Console_Control(void) +{ + UI_Console_Control(&m_UIState); +} + void Console_Draw(void) { - if (m_Console == nullptr) { - return; - } - - Console_ScrollLogs(); - - if (Console_IsOpened() || Console_GetVisibleLogCount() > 0) { - Console_DrawBackdrop(); - } - - m_Console->draw(m_Console); + UI_Console(&m_UIState); } diff --git a/src/libtrx/game/creature/common.c b/src/libtrx/game/creature/common.c index 866250f68..eedaa4e25 100644 --- a/src/libtrx/game/creature/common.c +++ b/src/libtrx/game/creature/common.c @@ -1,4 +1,231 @@ +#include "game/camera/vars.h" #include "game/creature.h" +#include "game/lara/common.h" +#include "game/los.h" +#include "game/objects/vars.h" +#include "game/pathing.h" +#include "game/random.h" +#include "game/rooms.h" +#include "log.h" +#include "utils.h" + +#define M_FLOAT_SPEED 32 +#define M_MAX_DISTANCE (WALL_L * 30) +#define M_ATTACK_RANGE SQUARE(WALL_L * 3) // = 0x900000 = 9437184 +#define M_ESCAPE_CHANCE 2048 +#define M_RECOVER_CHANCE 256 +#define M_TARGET_TOLERANCE 0x400000 +#define M_MAX_TILT (3 * DEG_1) // = 546 +#define M_MAX_HEAD_CHANGE (5 * DEG_1) // = 910 +#if TR_VERSION == 1 + #define M_HEAD_ARC FRONT_ARC +#elif TR_VERSION >= 2 + #define M_HEAD_ARC 0x3000 // = 12288 +#endif +#define M_MAX_X_ROT (20 * DEG_1) // = 3640 + +static bool m_AlliesHostile = false; + +static ITEM *M_ChooseEnemy(const ITEM *item); +static bool M_SwitchToWater( + int16_t item_num, const int32_t *wh, const HYBRID_INFO *info); +static bool M_SwitchToLand( + int16_t item_num, const int32_t *wh, const HYBRID_INFO *info); +static bool M_TestSwitchOrKill(int16_t item_num, GAME_OBJECT_ID target_id); + +#if TR_VERSION == 1 +extern void Carrier_TestItemDrops(int16_t item_num); +#endif + +static void M_GetBaddieTarget(const int16_t item_num, const bool goody) +{ + ITEM *const lara_item = Lara_GetItem(); + ITEM *const item = Item_Get(item_num); + CREATURE *const creature = item->data; + + ITEM *best_item = nullptr; + int32_t best_distance = INT32_MAX; + for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { + const int16_t target_item_num = LOT_GetBaddieSlot(i)->item_num; + if (target_item_num == NO_ITEM || target_item_num == item_num) { + continue; + } + + ITEM *const target = Item_Get(target_item_num); + const GAME_OBJECT_ID obj_id = target->object_id; +#if TR_VERSION == 2 + if (goody && obj_id != O_BANDIT_1 && obj_id != O_BANDIT_2) { + continue; + } else if (!goody && obj_id != O_MONK_1 && obj_id != O_MONK_2) { + continue; + } +#endif + + const int32_t dx = (target->pos.x - item->pos.x) >> 6; + const int32_t dy = (target->pos.y - item->pos.y) >> 6; + const int32_t dz = (target->pos.z - item->pos.z) >> 6; + const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); + if (distance < best_distance) { + best_item = target; + best_distance = distance; + } + } + + if (best_item == nullptr) { + if (!goody || Creature_AreAlliesHostile()) { + creature->enemy = lara_item; + } else { + creature->enemy = nullptr; + } + return; + } + + if (!goody || Creature_AreAlliesHostile()) { + const int32_t dx = (lara_item->pos.x - item->pos.x) >> 6; + const int32_t dy = (lara_item->pos.y - item->pos.y) >> 6; + const int32_t dz = (lara_item->pos.z - item->pos.z) >> 6; + const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); + if (distance < best_distance) { + best_item = lara_item; + best_distance = distance; + } + } + + const ITEM *const target = creature->enemy; + if (target == nullptr || target->status != IS_ACTIVE) { + creature->enemy = best_item; + } else { + const int32_t dx = (target->pos.x - item->pos.x) >> 6; + const int32_t dy = (target->pos.y - item->pos.y) >> 6; + const int32_t dz = (target->pos.z - item->pos.z) >> 6; + const int32_t distance = SQUARE(dz) + SQUARE(dy) + SQUARE(dx); + if (distance < best_distance + M_TARGET_TOLERANCE) { + creature->enemy = best_item; + } + } +} + +static ITEM *M_ChooseEnemy(const ITEM *const item) +{ + CREATURE *const creature = item->data; + switch (item->object_id) { +#if TR_VERSION == 2 + case O_BANDIT_1: + case O_BANDIT_2: + M_GetBaddieTarget(creature->item_num, false); + break; + + case O_MONK_1: + case O_MONK_2: + M_GetBaddieTarget(creature->item_num, true); + break; +#endif + + default: + creature->enemy = Lara_GetItem(); + break; + } + + if (creature->enemy != nullptr) { + return creature->enemy; + } + return Lara_GetItem(); +} + +static bool M_SwitchToWater( + const int16_t item_num, const int32_t *const wh, + const HYBRID_INFO *const info) +{ + if (*wh == NO_HEIGHT) { + return false; + } + + ITEM *const item = Item_Get(item_num); + + if (item->hit_points <= 0) { + // Dead land creatures should remain in their pose permanently. + return false; + } + + // The land creature is alive and the room has been flooded. Switch to the + // water creature. + if (!M_TestSwitchOrKill(item_num, info->water.id)) { + return false; + } + + item->object_id = info->water.id; + Item_SwitchToAnim(item, info->water.active_anim, 0); + item->current_anim_state = Item_GetAnim(item)->current_anim_state; + item->goal_anim_state = item->current_anim_state; + item->pos.y = *wh; + + return true; +} + +static bool M_SwitchToLand( + const int16_t item_num, const int32_t *const wh, + const HYBRID_INFO *const info) +{ + if (*wh != NO_HEIGHT) { + return false; + } + + if (!M_TestSwitchOrKill(item_num, info->land.id)) { + return false; + } + + ITEM *const item = Item_Get(item_num); + + // Switch to the land creature regardless of death state. + item->object_id = info->land.id; + item->rot.x = 0; + + if (item->hit_points > 0) { + Item_SwitchToAnim(item, info->land.active_anim, 0); + item->current_anim_state = Item_GetAnim(item)->current_anim_state; + item->goal_anim_state = item->current_anim_state; + + } else { + Item_SwitchToAnim(item, info->land.death_anim, -1); + item->current_anim_state = info->land.death_state; + item->goal_anim_state = item->current_anim_state; + + int16_t room_num = item->room_num; + const SECTOR *const sector = + Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); + item->floor = + Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); + item->pos.y = item->floor; + + if (item->room_num != room_num) { + Item_NewRoom(item_num, room_num); + } + } + + return true; +} + +static bool M_TestSwitchOrKill( + const int16_t item_num, const GAME_OBJECT_ID target_id) +{ + if (Object_Get(target_id)->loaded) { + return true; + } + + LOG_WARNING( + "Object %d is not loaded; item %d cannot be converted.", target_id, + item_num); + Item_Kill(item_num); + return false; +} + +void Creature_Initialise(const int16_t item_num) +{ + ITEM *const item = Item_Get(item_num); + item->rot.y += (Random_GetControl() - DEG_90) >> 1; + item->collidable = 1; + item->data = nullptr; +} bool Creature_Activate(const int16_t item_num) { @@ -14,3 +241,880 @@ bool Creature_Activate(const int16_t item_num) item->status = IS_ACTIVE; return true; } + +void Creature_AIInfo(ITEM *const item, AI_INFO *const info) +{ + CREATURE *const creature = item->data; + if (creature == nullptr) { + return; + } + + ITEM *const enemy = M_ChooseEnemy(item); + const int16_t *const zone = Box_GetLotZone(&creature->lot); + + { + const ROOM *const room = Room_Get(item->room_num); + item->box_num = + Room_GetWorldSector(room, item->pos.x, item->pos.z)->box; + info->zone_num = zone[item->box_num]; + } + + { + const ROOM *const room = Room_Get(enemy->room_num); + enemy->box_num = + Room_GetWorldSector(room, enemy->pos.x, enemy->pos.z)->box; + info->enemy_zone_num = zone[enemy->box_num]; + } + + if (((Box_GetBox(enemy->box_num)->overlap_index & creature->lot.block_mask) + != 0) + || (creature->lot.node[item->box_num].search_num + == (creature->lot.search_num | BOX_BLOCKED_SEARCH))) { + info->enemy_zone_num |= BOX_BLOCKED; + } + + const OBJECT *const obj = Object_Get(item->object_id); + const int32_t z = enemy->pos.z + - ((obj->pivot_length * Math_Cos(item->rot.y)) >> W2V_SHIFT) + - item->pos.z; + const int32_t x = enemy->pos.x + - ((obj->pivot_length * Math_Sin(item->rot.y)) >> W2V_SHIFT) + - item->pos.x; + const int16_t angle = Math_Atan(z, x); + + if (creature->enemy == nullptr) { + info->distance = 0x7FFFFFFF; + } else if (ABS(x) > M_MAX_DISTANCE || ABS(z) > M_MAX_DISTANCE) { + info->distance = SQUARE(M_MAX_DISTANCE); + } else { + info->distance = SQUARE(x) + SQUARE(z); + } + + info->angle = angle - item->rot.y; + info->enemy_facing = angle - enemy->rot.y + DEG_180; + info->ahead = info->angle > -FRONT_ARC && info->angle < FRONT_ARC; + info->bite = info->ahead && ABS(enemy->pos.y - item->pos.y) <= STEP_L + && (TR_VERSION == 1 || enemy->hit_points > 0); +} + +bool Creature_EnsureHabitat( + const int16_t item_num, int32_t *const wh, const HYBRID_INFO *const info) +{ + // Test the environment for a hybrid creature. Record the water height and + // return whether or not a type conversion has taken place. + const ITEM *const item = Item_Get(item_num); + *wh = Room_GetWaterHeight( + item->pos.x, item->pos.y, item->pos.z, item->room_num); + + return item->object_id == info->land.id + ? M_SwitchToWater(item_num, wh, info) + : M_SwitchToLand(item_num, wh, info); +} + +void Creature_Mood( + const ITEM *const item, const AI_INFO *const info, const bool violent) +{ + CREATURE *const creature = item->data; + if (creature == nullptr) { + return; + } + + LOT_INFO *const lot = &creature->lot; + const ITEM *enemy = TR_VERSION >= 2 ? creature->enemy : Lara_GetItem(); + if (lot->node[item->box_num].search_num + == (lot->search_num | BOX_BLOCKED_SEARCH)) { + lot->required_box = NO_BOX; + } + + if (creature->mood != MOOD_ATTACK && lot->required_box != NO_BOX + && !Box_ValidBox(item, info->zone_num, lot->target_box)) { + if (info->zone_num == info->enemy_zone_num) { + creature->mood = MOOD_BORED; + } + lot->required_box = NO_BOX; + } + + const MOOD_TYPE mood = creature->mood; + if (enemy == nullptr) { + creature->mood = MOOD_BORED; + enemy = Lara_GetItem(); + } else if (enemy->hit_points <= 0) { + creature->mood = MOOD_BORED; + } else if (violent) { + switch (mood) { + case MOOD_BORED: + case MOOD_STALK: + if (info->zone_num == info->enemy_zone_num) { + creature->mood = MOOD_ATTACK; + } else if (item->hit_status) { + creature->mood = MOOD_ESCAPE; + } + break; + + case MOOD_ATTACK: + if (info->zone_num != info->enemy_zone_num) { + creature->mood = MOOD_BORED; + } + break; + + case MOOD_ESCAPE: + if (info->zone_num == info->enemy_zone_num) { + creature->mood = MOOD_ATTACK; + } + break; + } + } else { + switch (mood) { + case MOOD_BORED: + case MOOD_STALK: + if (item->hit_status + && (Random_GetControl() < M_ESCAPE_CHANCE + || info->zone_num != info->enemy_zone_num)) { + creature->mood = MOOD_ESCAPE; + } else if (info->zone_num == info->enemy_zone_num) { + if (info->distance < M_ATTACK_RANGE + || (creature->mood == MOOD_STALK + && lot->required_box == NO_BOX)) { + creature->mood = MOOD_ATTACK; + } else { + creature->mood = MOOD_STALK; + } + } + break; + + case MOOD_ATTACK: + if (item->hit_status + && (Random_GetControl() < M_ESCAPE_CHANCE + || info->zone_num != info->enemy_zone_num)) { + creature->mood = MOOD_ESCAPE; + } else if (info->zone_num != info->enemy_zone_num) { + creature->mood = MOOD_BORED; + } + break; + + case MOOD_ESCAPE: + if (info->zone_num == info->enemy_zone_num + && Random_GetControl() < M_RECOVER_CHANCE) { + creature->mood = MOOD_STALK; + } + break; + } + } + + if (mood != creature->mood) { + if (mood == MOOD_ATTACK) { + Box_TargetBox(lot, lot->target_box); + } + lot->required_box = NO_BOX; + } + + switch (creature->mood) { + case MOOD_BORED: { + const int16_t box_num = + lot->node[lot->zone_count * Random_GetControl() / 0x7FFF].box_num; + if (Box_ValidBox(item, info->zone_num, box_num)) { + if (Box_StalkBox(item, enemy, box_num) + && (TR_VERSION == 1 + || (creature->enemy != nullptr && enemy->hit_points > 0))) { + Box_TargetBox(lot, box_num); + creature->mood = MOOD_STALK; + } else if (lot->required_box == NO_BOX) { + Box_TargetBox(lot, box_num); + } + } + break; + } + + case MOOD_ATTACK: + if (TR_VERSION >= 2 + || Random_GetControl() < Object_Get(item->object_id)->smartness) { + lot->target = enemy->pos; + lot->required_box = enemy->box_num; + if (lot->fly != 0 + && Lara_GetLaraInfo()->water_status == LWS_ABOVE_WATER) { + lot->target.y += Item_GetBestFrame(enemy)->bounds.min.y; + } + } + break; + + case MOOD_ESCAPE: { + const int16_t box_num = + lot->node[lot->zone_count * Random_GetControl() / 0x7FFF].box_num; + if (Box_ValidBox(item, info->zone_num, box_num) + && lot->required_box == NO_BOX) { + if (Box_EscapeBox(item, enemy, box_num)) { + Box_TargetBox(lot, box_num); + } else if ( + info->zone_num == info->enemy_zone_num + && Box_StalkBox(item, enemy, box_num)) { + Box_TargetBox(lot, box_num); + creature->mood = MOOD_STALK; + } + } + break; + } + + case MOOD_STALK: { + if (lot->required_box == NO_BOX + || !Box_StalkBox(item, enemy, lot->required_box)) { + const int16_t box_num = + lot->node[lot->zone_count * Random_GetControl() / 0x7FFF] + .box_num; + if (Box_ValidBox(item, info->zone_num, box_num)) { + if (Box_StalkBox(item, enemy, box_num)) { + Box_TargetBox(lot, box_num); + } else if (lot->required_box == NO_BOX) { + Box_TargetBox(lot, box_num); + if (info->zone_num != info->enemy_zone_num) { + creature->mood = MOOD_BORED; + } + } + } + } + break; + } + } + + if (lot->target_box == NO_BOX) { + Box_TargetBox(lot, item->box_num); + } + Box_CalculateTarget(&creature->target, item, lot); +} + +int16_t Creature_Turn(ITEM *const item, int16_t max_turn) +{ + const CREATURE *const creature = item->data; + if (creature == nullptr || item->speed == 0 || max_turn == 0) { + return 0; + } + + const int32_t dx = creature->target.x - item->pos.x; + const int32_t dz = creature->target.z - item->pos.z; + + int16_t angle = Math_Atan(dz, dx) - item->rot.y; + if (angle > FRONT_ARC || angle < -FRONT_ARC) { + const int32_t range = (item->speed << 14) / max_turn; + if (SQUARE(dx) + SQUARE(dz) < SQUARE(range)) { + max_turn /= 2; + } + } + + CLAMP(angle, -max_turn, max_turn); + item->rot.y += angle; + return angle; +} + +void Creature_Tilt(ITEM *const item, int16_t angle) +{ + angle = angle * 4 - item->rot.z; + CLAMP(angle, -M_MAX_TILT, M_MAX_TILT); + item->rot.z += angle; +} + +void Creature_Head(ITEM *const item, const int16_t required) +{ + CREATURE *const creature = item->data; + if (creature == nullptr) { + return; + } + + int16_t change = required - creature->head_rotation; + CLAMP(change, -M_MAX_HEAD_CHANGE, M_MAX_HEAD_CHANGE); + + creature->head_rotation += change; + CLAMP(creature->head_rotation, -M_HEAD_ARC, M_HEAD_ARC); +} + +void Creature_Neck(ITEM *const item, const int16_t required) +{ + CREATURE *const creature = item->data; + if (creature == nullptr) { + return; + } + + int16_t change = required - creature->neck_rotation; + CLAMP(change, -M_MAX_HEAD_CHANGE, M_MAX_HEAD_CHANGE); + + creature->neck_rotation += change; + CLAMP(creature->neck_rotation, -M_HEAD_ARC, M_HEAD_ARC); +} + +void Creature_Float(const int16_t item_num) +{ + ITEM *const item = Item_Get(item_num); + + item->hit_points = DONT_TARGET; + item->rot.x = 0; + + const int32_t wh = Room_GetWaterHeight( + item->pos.x, item->pos.y, item->pos.z, item->room_num); + if (item->pos.y > wh) { + item->pos.y -= M_FLOAT_SPEED; + } else if (item->pos.y < wh) { + item->pos.y = wh; + } + + Item_Animate(item); + + int16_t room_num = item->room_num; + const SECTOR *const sector = + Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); + item->floor = Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); + if (room_num != item->room_num) { + Item_NewRoom(item_num, room_num); + } +} + +void Creature_Underwater(ITEM *const item, const int32_t depth) +{ + const int32_t wh = Room_GetWaterHeight( + item->pos.x, item->pos.y, item->pos.z, item->room_num); + if (item->pos.y >= wh + depth) { + return; + } + + item->pos.y = wh + depth; + if (item->rot.x > 2 * DEG_1) { + item->rot.x -= 2 * DEG_1; + } else { + CLAMPG(item->rot.x, 0); + } +} + +bool Creature_CanTargetEnemy(const ITEM *const item, const AI_INFO *const info) +{ + const CREATURE *const creature = item->data; + const ITEM *const enemy = + creature->enemy != nullptr ? creature->enemy : Lara_GetItem(); + if (!info->ahead || info->distance >= CREATURE_SHOOT_RANGE) { + return false; + } + + GAME_VECTOR start; + start.pos.x = item->pos.x; + start.pos.y = item->pos.y - STEP_L * 3; + start.pos.z = item->pos.z; + start.room_num = item->room_num; + + GAME_VECTOR target; + target.pos.x = enemy->pos.x; + target.pos.y = enemy->pos.y - STEP_L * 3; + target.pos.z = enemy->pos.z; + target.room_num = enemy->room_num; + + return LOS_Check(&start, &target); +} + +bool Creature_CheckBaddieOverlap(const int16_t item_num) +{ + const ITEM *item = Item_Get(item_num); + + const int32_t x = item->pos.x; + const int32_t y = item->pos.y; + const int32_t z = item->pos.z; + const int32_t radius = SQUARE(Object_Get(item->object_id)->radius); + + int16_t link = Room_Get(item->room_num)->item_num; + while (link != NO_ITEM && link != item_num) { + item = Item_Get(link); + if (item != Lara_GetItem() && item->status == IS_ACTIVE + && item->speed != 0) { + const XYZ_32 delta = { + item->pos.x - x, + item->pos.y - y, + item->pos.z - z, + }; + const int32_t distance = + SQUARE(delta.x) + SQUARE(delta.y) + SQUARE(delta.z); + if (distance < radius) { + return true; + } + } + + link = item->next_item; + } + + return false; +} + +void Creature_Collision( + const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) +{ + ITEM *const item = Item_Get(item_num); + const LARA_INFO *const lara = Lara_GetLaraInfo(); + if (!Lara_TestBoundsCollide(item, coll->radius)) { + return; + } + if (!Collide_TestCollision(item, lara_item)) { + return; + } + + if (!coll->enable_baddie_push) { + return; + } + + if (TR_VERSION >= 2 + && (lara->water_status == LWS_UNDERWATER + || lara->water_status == LWS_SURFACE)) { + return; + } + + Lara_Push( + item, coll, + (TR_VERSION >= 2 || item->hit_points > 0) ? coll->enable_hit : false, + false); +} + +bool Creature_Animate( + const int16_t item_num, const int16_t angle, const int16_t tilt) +{ + ITEM *const item = Item_Get(item_num); + const CREATURE *const creature = item->data; + const OBJECT *const obj = Object_Get(item->object_id); + if (creature == nullptr) { + return false; + } + + const LOT_INFO *const lot = &creature->lot; + const XYZ_32 old = item->pos; + + const int16_t *const zone = Box_GetLotZone(lot); + + if (TR_VERSION >= 2 && !Object_IsType(item->object_id, g_WaterObjects)) { + int16_t room_num = item->room_num; + Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); + if (room_num != item->room_num) { + Item_NewRoom(item_num, room_num); + } + } + + Item_Animate(item); + if (item->status == IS_DEACTIVATED) { + Creature_Die(item_num, false); + return false; + } + + const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); + int32_t y = item->pos.y + bounds->min.y; + + int16_t room_num = item->room_num; + Room_GetSector(old.x, y, old.z, &room_num); + const SECTOR *sector = + Room_GetSector(item->pos.x, y, item->pos.z, &room_num); + int32_t height = Box_GetBox(sector->box)->height; + int16_t next_box = lot->node[sector->box].exit_box; + int32_t next_height = + next_box != NO_BOX ? Box_GetBox(next_box)->height : height; + + const int32_t box_height = Box_GetBox(item->box_num)->height; + if (sector->box == NO_BOX || zone[item->box_num] != zone[sector->box] + || box_height - height > lot->step || box_height - height < lot->drop) { + const int32_t pos_x = item->pos.x >> WALL_SHIFT; + const int32_t shift_x = old.x >> WALL_SHIFT; + const int32_t shift_z = old.z >> WALL_SHIFT; + + if (pos_x < shift_x) { + item->pos.x = old.x & (~(WALL_L - 1)); + } else if (pos_x > shift_x) { + item->pos.x = old.x | (WALL_L - 1); + } + + if (pos_x < shift_z) { + item->pos.z = old.z & (~(WALL_L - 1)); + } else if (pos_x > shift_z) { + item->pos.z = old.z | (WALL_L - 1); + } + + sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); + height = Box_GetBox(sector->box)->height; + next_box = lot->node[sector->box].exit_box; + next_height = + next_box != NO_BOX ? Box_GetBox(next_box)->height : height; + } + + const int32_t x = item->pos.x; + const int32_t z = item->pos.z; + const int32_t pos_x = x & (WALL_L - 1); + const int32_t pos_z = z & (WALL_L - 1); + int32_t shift_x = 0; + int32_t shift_z = 0; + const int32_t radius = obj->radius; + + if (pos_z < radius) { + if (Box_BadFloor( + x, y, z - radius, height, next_height, room_num, lot)) { + shift_z = radius - pos_z; + } + + if (pos_x < radius) { + if (Box_BadFloor( + x - radius, y, z, height, next_height, room_num, lot)) { + shift_x = radius - pos_x; + } else if ( + !shift_z + && Box_BadFloor( + x - radius, y, z - radius, height, next_height, room_num, + lot)) { + if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { + shift_z = radius - pos_z; + } else { + shift_x = radius - pos_x; + } + } + } else if (pos_x > WALL_L - radius) { + if (Box_BadFloor( + x + radius, y, z, height, next_height, room_num, lot)) { + shift_x = WALL_L - radius - pos_x; + } else if ( + !shift_z + && Box_BadFloor( + x + radius, y, z - radius, height, next_height, room_num, + lot)) { + if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { + shift_z = radius - pos_z; + } else { + shift_x = WALL_L - radius - pos_x; + } + } + } + } else if (pos_z > WALL_L - radius) { + if (Box_BadFloor( + x, y, z + radius, height, next_height, room_num, lot)) { + shift_z = WALL_L - radius - pos_z; + } + + if (pos_x < radius) { + if (Box_BadFloor( + x - radius, y, z, height, next_height, room_num, lot)) { + shift_x = radius - pos_x; + } else if ( + !shift_z + && Box_BadFloor( + x - radius, y, z + radius, height, next_height, room_num, + lot)) { + if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { + shift_x = radius - pos_x; + } else { + shift_z = WALL_L - radius - pos_z; + } + } + } else if (pos_x > WALL_L - radius) { + if (Box_BadFloor( + x + radius, y, z, height, next_height, room_num, lot)) { + shift_x = WALL_L - radius - pos_x; + } else if ( + !shift_z + && Box_BadFloor( + x + radius, y, z + radius, height, next_height, room_num, + lot)) { + if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { + shift_x = WALL_L - radius - pos_x; + } else { + shift_z = WALL_L - radius - pos_z; + } + } + } + } else if (pos_x < radius) { + if (Box_BadFloor( + x - radius, y, z, height, next_height, room_num, lot)) { + shift_x = radius - pos_x; + } + } else if (pos_x > WALL_L - radius) { + if (Box_BadFloor( + x + radius, y, z, height, next_height, room_num, lot)) { + shift_x = WALL_L - radius - pos_x; + } + } + + item->pos.x += shift_x; + item->pos.z += shift_z; + + if (shift_x || shift_z) { + sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); + item->rot.y += angle; + Creature_Tilt(item, tilt * 2); + } + + if (Creature_CheckBaddieOverlap(item_num)) { + item->pos = old; + return true; + } + + if (lot->fly) { + int32_t dy = creature->target.y - item->pos.y; + CLAMP(dy, -lot->fly, lot->fly); + + height = Room_GetHeight(sector, item->pos.x, y, item->pos.z); + if (item->pos.y + dy <= height) { + const int32_t ceiling = + Room_GetCeiling(sector, item->pos.x, y, item->pos.z); + int32_t min_y = bounds->min.y; + switch (item->object_id) { +#if TR_VERSION == 1 + case O_ALLIGATOR: + min_y = 0; + break; +#elif TR_VERSION == 2 + case O_SHARK: + min_y = 128; + break; +#endif + default: + break; + } + if (item->pos.y + min_y + dy < ceiling) { + if (item->pos.y + min_y < ceiling) { + item->pos.x = old.x; + item->pos.z = old.z; + dy = lot->fly; + } else { + dy = 0; + } + } + } else if (item->pos.y <= height) { + item->pos.y = height; + dy = 0; + } else { + item->pos.x = old.x; + item->pos.z = old.z; + dy = -lot->fly; + } + + item->pos.y += dy; + sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); + item->floor = Room_GetHeight(sector, item->pos.x, y, item->pos.z); + + int16_t angle = item->speed ? Math_Atan(item->speed, -dy) : 0; + if (TR_VERSION >= 2) { + CLAMP(angle, -M_MAX_X_ROT, M_MAX_X_ROT); + } + + if (angle < item->rot.x - DEG_1) { + item->rot.x -= DEG_1; + } else if (angle > item->rot.x + DEG_1) { + item->rot.x += DEG_1; + } else { + item->rot.x = angle; + } + } else { + const SECTOR *const sector = + Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); + item->floor = + Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); + + if (item->pos.y > item->floor) { + item->pos.y = item->floor; + } else if (item->floor - item->pos.y > STEP_L / 4) { + item->pos.y += STEP_L / 4; + } else if (item->pos.y < item->floor) { + item->pos.y = item->floor; + } + item->rot.x = 0; + } + + if (!Object_IsType(item->object_id, g_WaterObjects)) { + // Get the room just above the enemy so that if it is in one-click high + // water, its effects behave still as though in a dry room. + Room_GetSector( + item->pos.x, item->pos.y - (STEP_L * 2), item->pos.z, &room_num); + if (TR_VERSION >= 2 + && (Room_Get(room_num)->flags & RF_UNDERWATER) != 0) { + item->hit_points = 0; + } + } + + if (item->room_num != room_num) { + Item_NewRoom(item_num, room_num); + } + return true; +} + +void Creature_SpecialKill( + ITEM *const item, const int32_t kill_anim, const int32_t kill_state, + const int32_t lara_kill_state) +{ + LARA_INFO *const lara = Lara_GetLaraInfo(); + ITEM *const lara_item = Lara_GetItem(); + + Item_SwitchToAnim(item, kill_anim, 0); + item->current_anim_state = kill_state; + +#if TR_VERSION == 2 + Item_SwitchToObjAnim(lara_item, LA_EXTRA_BREATH, 0, O_LARA_EXTRA); + lara_item->current_anim_state = LA_EXTRA_BREATH; +#endif + lara_item->goal_anim_state = lara_kill_state; + lara_item->pos = item->pos; + lara_item->rot = item->rot; + lara_item->fall_speed = 0; + lara_item->gravity = 0; + lara_item->speed = 0; + + int16_t room_num = item->room_num; + if (room_num != lara_item->room_num) { + Item_NewRoom(lara->item_num, room_num); + } + + Item_Animate(lara_item); + + lara_item->goal_anim_state = lara_kill_state; + lara_item->current_anim_state = lara_kill_state; +#if TR_VERSION == 2 + lara->extra_anim = 1; +#endif + lara->gun_status = LGS_HANDS_BUSY; + lara->gun_type = LGT_UNARMED; + lara->hit_direction = -1; + lara->air = -1; + + g_Camera.pos.room_num = lara_item->room_num; +} + +void Creature_Die(const int16_t item_num, const bool explode) +{ + ITEM *const item = Item_Get(item_num); + + switch (item->object_id) { +#if TR_VERSION == 2 + case O_DRAGON_FRONT: + item->hit_points = 0; + return; + + case O_SKIDOO_DRIVER: + if (explode) { + Item_Explode(item_num, -1, 0); + } + item->hit_points = DONT_TARGET; + const int16_t vehicle_item_num = (int16_t)(intptr_t)item->data; + ITEM *const vehicle_item = Item_Get(vehicle_item_num); + vehicle_item->hit_points = 0; + return; +#endif + + default: + break; + } + + item->collidable = 0; + item->hit_points = DONT_TARGET; + if (explode) { + Item_Explode(item_num, -1, 0); + Item_Kill(item_num); + } else { + Item_RemoveActive(item_num); + } + + const OBJECT *const obj = Object_Get(item->object_id); + if (obj->intelligent) { + LOT_DisableBaddieAI(item_num); + } + item->flags |= IF_ONE_SHOT; + +#if TR_VERSION >= 2 + if (item->killed) { + item->next_active = Item_GetPrevActive(); + Item_SetPrevActive(item_num); + } + + if (obj->intelligent) { + int16_t pickup_num = item->carried_item; + while (pickup_num != NO_ITEM) { + ITEM *const pickup = Item_Get(pickup_num); + pickup->pos = item->pos; + Item_NewRoom(pickup_num, item->room_num); + pickup_num = pickup->carried_item; + } + } +#else + Carrier_TestItemDrops(item_num); +#endif +} + +int32_t Creature_Vault( + const int16_t item_num, const int16_t angle, int32_t vault, + const int32_t shift) +{ + ITEM *const item = Item_Get(item_num); + const int16_t room_num = item->room_num; + const XYZ_32 old = item->pos; + + Creature_Animate(item_num, angle, 0); + + if (item->floor > old.y + STEP_L * 7 / 2) { + vault = -4; + } else if (item->pos.y > old.y - STEP_L * 3 / 2) { + return 0; + } else if (item->pos.y > old.y - STEP_L * 5 / 2) { + vault = 2; + } else if (item->pos.y > old.y - STEP_L * 7 / 2) { + vault = 3; + } else { + vault = 4; + } + + const int32_t old_x_sector = old.x >> WALL_SHIFT; + const int32_t old_z_sector = old.z >> WALL_SHIFT; + const int32_t x_sector = item->pos.x >> WALL_SHIFT; + const int32_t z_sector = item->pos.z >> WALL_SHIFT; + if (old_z_sector == z_sector) { + if (old_x_sector == x_sector) { + return 0; + } + + if (old_x_sector >= x_sector) { + item->rot.y = -DEG_90; + item->pos.x = (old_x_sector << WALL_SHIFT) + shift; + } else { + item->rot.y = DEG_90; + item->pos.x = (x_sector << WALL_SHIFT) - shift; + } + } else if (old_x_sector == x_sector) { + if (old_z_sector >= z_sector) { + item->rot.y = -DEG_180; + item->pos.z = (old_z_sector << WALL_SHIFT) + shift; + } else { + item->rot.y = 0; + item->pos.z = (z_sector << WALL_SHIFT) - shift; + } + } + + item->floor = old.y; + item->pos.y = old.y; + + if (room_num != item->room_num) { + Item_NewRoom(item_num, room_num); + } + return vault; +} + +bool Creature_AreAlliesHostile(void) +{ + return m_AlliesHostile; +} + +void Creature_SetAlliesHostile(bool enable) +{ + m_AlliesHostile = enable; +} + +bool Creature_IsHostile(const ITEM *const item) +{ + return Object_IsType(item->object_id, g_EnemyObjects) + || (Creature_AreAlliesHostile() && Creature_IsAlly(item)); +} + +bool Creature_IsAlly(const ITEM *const item) +{ + return Object_IsType(item->object_id, g_AllyObjects); +} + +int16_t Creature_Effect( + const ITEM *const item, const BITE *const bite, + int16_t (*const spawn)( + int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, + int16_t room_num)) +{ + XYZ_32 pos = bite->pos; + Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); + return spawn(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num); +} diff --git a/src/libtrx/game/game.c b/src/libtrx/game/game.c index c8c54120b..d7fd1c07d 100644 --- a/src/libtrx/game/game.c +++ b/src/libtrx/game/game.c @@ -6,6 +6,7 @@ static bool m_IsPlaying = false; static const GF_LEVEL *m_CurrentLevel = nullptr; +static GAME_BONUS_FLAG m_BonusFlag = GBF_NONE; void Game_SetIsPlaying(const bool is_playing) { @@ -49,3 +50,18 @@ bool Game_IsPlayable(void) return true; } + +GAME_BONUS_FLAG Game_GetBonusFlag(void) +{ + return m_BonusFlag; +} + +void Game_SetBonusFlag(const GAME_BONUS_FLAG flag) +{ + m_BonusFlag = flag; +} + +bool Game_IsBonusFlagSet(const GAME_BONUS_FLAG flag) +{ + return (m_BonusFlag & flag) != 0; +} diff --git a/src/libtrx/game/game_flow/common.c b/src/libtrx/game/game_flow/common.c index cc5b08003..ab17d7a1d 100644 --- a/src/libtrx/game/game_flow/common.c +++ b/src/libtrx/game/game_flow/common.c @@ -91,9 +91,8 @@ void GF_Shutdown(void) Memory_FreePointer(&gf->main_menu_background_path); Memory_FreePointer(&gf->savegame_fmt_legacy); -#if TR_VERSION == 1 Memory_FreePointer(&gf->savegame_fmt_bson); -#else +#if TR_VERSION == 2 Memory_FreePointer(&gf->settings.sfx_path); #endif } diff --git a/src/libtrx/game/game_flow/reader.c b/src/libtrx/game/game_flow/reader.c index 8ebdfd4d5..26f096e74 100644 --- a/src/libtrx/game/game_flow/reader.c +++ b/src/libtrx/game/game_flow/reader.c @@ -11,6 +11,7 @@ #include "json.h" #include "log.h" #include "memory.h" +#include "strings.h" #include @@ -71,6 +72,7 @@ static void M_LoadFMV( void *user_arg); static void M_LoadFMVs(JSON_OBJECT *obj, GAME_FLOW *gf); static void M_LoadGlobalInjections(JSON_OBJECT *obj, GAME_FLOW *gf); +static void M_LoadCommonSettings(JSON_OBJECT *obj, GF_LEVEL_SETTINGS *settings); static void M_LoadCommonRoot(JSON_OBJECT *obj, GAME_FLOW *gf); static void M_LoadRoot(JSON_OBJECT *obj, GAME_FLOW *gf); @@ -80,6 +82,59 @@ static void M_LoadRoot(JSON_OBJECT *obj, GAME_FLOW *gf); #include "./reader_tr2.def.c" #endif +static void M_LoadCommonSettings( + JSON_OBJECT *const obj, GF_LEVEL_SETTINGS *const settings) +{ + { + const double value = + JSON_ObjectGetDouble(obj, "fog_start", JSON_INVALID_NUMBER); + if (value != JSON_INVALID_NUMBER) { + settings->fog_start.is_present = true; + settings->fog_start.value = value; + } + } + + { + const double value = + JSON_ObjectGetDouble(obj, "fog_end", JSON_INVALID_NUMBER); + if (value != JSON_INVALID_NUMBER) { + settings->fog_end.is_present = true; + settings->fog_end.value = value; + } + } + + { + JSON_VALUE *const tmp_value = JSON_ObjectGetValue(obj, "water_color"); + if (tmp_value != nullptr && tmp_value->type == JSON_TYPE_ARRAY) { + const JSON_ARRAY *const tmp_arr = JSON_ValueAsArray(tmp_value); + const RGB_F color = { + JSON_ArrayGetDouble(tmp_arr, 0, JSON_INVALID_NUMBER), + JSON_ArrayGetDouble(tmp_arr, 1, JSON_INVALID_NUMBER), + JSON_ArrayGetDouble(tmp_arr, 2, JSON_INVALID_NUMBER), + }; + if (color.r != JSON_INVALID_NUMBER && color.g != JSON_INVALID_NUMBER + && color.b != JSON_INVALID_NUMBER) { + settings->water_color.is_present = true; + settings->water_color.value = (RGB_888) { + color.r * 255.0f, + color.g * 255.0f, + color.b * 255.0f, + }; + } + } else if ( + tmp_value != nullptr && tmp_value->type == JSON_TYPE_STRING) { + const char *tmp_str = + JSON_ValueGetString(tmp_value, JSON_INVALID_STRING); + ASSERT(tmp_str != JSON_INVALID_STRING); + RGB_888 tmp_color; + if (String_ParseRGB888(tmp_str, &tmp_color)) { + settings->water_color.is_present = true; + settings->water_color.value = tmp_color; + } + } + } +} + static void M_LoadCommonRoot(JSON_OBJECT *const obj, GAME_FLOW *const gf) { const char *tmp_s = @@ -95,6 +150,12 @@ static void M_LoadCommonRoot(JSON_OBJECT *const obj, GAME_FLOW *const gf) Shell_ExitSystem("'savegame_fmt_legacy' must be a string"); } gf->savegame_fmt_legacy = Memory_DupStr(tmp_s); + + tmp_s = JSON_ObjectGetString(obj, "savegame_fmt_bson", JSON_INVALID_STRING); + if (tmp_s == JSON_INVALID_STRING) { + Shell_ExitSystem("'savegame_fmt_bson' must be a string"); + } + gf->savegame_fmt_bson = Memory_DupStr(tmp_s); } static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleIntEvent) @@ -109,23 +170,23 @@ static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleIntEvent) static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandlePictureEvent) { const char *const path = JSON_ObjectGetString(event_obj, "path", nullptr); - if (path == nullptr) { - Shell_ExitSystem("Missing picture path"); - return -1; - } if (event != nullptr) { GF_DISPLAY_PICTURE_DATA *const event_data = extra_data; event_data->path = (char *)extra_data + sizeof(GF_DISPLAY_PICTURE_DATA); + event_data->is_legal = JSON_ObjectGetBool(event_obj, "legal", false); event_data->display_time = JSON_ObjectGetDouble(event_obj, "display_time", 5.0); event_data->fade_in_time = JSON_ObjectGetDouble(event_obj, "fade_in_time", 1.0); event_data->fade_out_time = JSON_ObjectGetDouble(event_obj, "fade_out_time", 1.0 / 3.0); - strcpy(event_data->path, path); + if (path != nullptr) { + strcpy(event_data->path, path); + } event->data = event_data; } - return sizeof(GF_DISPLAY_PICTURE_DATA) + strlen(path) + 1; + return sizeof(GF_DISPLAY_PICTURE_DATA) + + (path == nullptr ? 0 : strlen(path) + 1); } static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleTotalStatsEvent) @@ -133,8 +194,10 @@ static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleTotalStatsEvent) const char *const path = JSON_ObjectGetString(event_obj, "background_path", nullptr); if (path == nullptr) { - Shell_ExitSystem("Missing picture path"); - return -1; + if (event != nullptr) { + event->data = nullptr; + } + return 0; } if (event != nullptr) { char *const event_data = extra_data; @@ -465,6 +528,7 @@ static void M_LoadFMV( Shell_ExitSystemFmt("Missing FMV path"); } fmv->path = Memory_DupStr(path); + fmv->is_legal = JSON_ObjectGetBool(obj, "legal", false); } static void M_LoadFMVs(JSON_OBJECT *const obj, GAME_FLOW *const gf) diff --git a/src/libtrx/game/game_flow/reader_tr1.def.c b/src/libtrx/game/game_flow/reader_tr1.def.c index 76786a3c7..983e13032 100644 --- a/src/libtrx/game/game_flow/reader_tr1.def.c +++ b/src/libtrx/game/game_flow/reader_tr1.def.c @@ -7,12 +7,6 @@ static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleMeshSwapEvent); static void M_LoadLevelItemDrops( JSON_OBJECT *obj, const GAME_FLOW *gf, GF_LEVEL *level); -static GF_LEVEL_SETTINGS m_DefaultSettings = { - .water_color = { .r = 0.6, .g = 0.7, .b = 1.0 }, - .draw_distance_fade = 12.0f, - .draw_distance_max = 20.0f, -}; - static M_SEQUENCE_EVENT_HANDLER m_SequenceEventHandlers[] = { // clang-format off // Events without arguments @@ -104,33 +98,7 @@ static DECLARE_SEQUENCE_EVENT_HANDLER_FUNC(M_HandleMeshSwapEvent) static void M_LoadSettings( JSON_OBJECT *const obj, GF_LEVEL_SETTINGS *const settings) { - { - const double value = JSON_ObjectGetDouble( - obj, "draw_distance_fade", JSON_INVALID_NUMBER); - if (value != JSON_INVALID_NUMBER) { - settings->draw_distance_fade = value; - } - } - - { - const double value = - JSON_ObjectGetDouble(obj, "draw_distance_max", JSON_INVALID_NUMBER); - if (value != JSON_INVALID_NUMBER) { - settings->draw_distance_max = value; - } - } - - { - JSON_ARRAY *const tmp_arr = JSON_ObjectGetArray(obj, "water_color"); - if (tmp_arr != nullptr) { - settings->water_color.r = - JSON_ArrayGetDouble(tmp_arr, 0, settings->water_color.r); - settings->water_color.g = - JSON_ArrayGetDouble(tmp_arr, 1, settings->water_color.g); - settings->water_color.b = - JSON_ArrayGetDouble(tmp_arr, 2, settings->water_color.b); - } - } + M_LoadCommonSettings(obj, settings); } static void M_LoadLevelGameSpecifics( @@ -228,23 +196,15 @@ static void M_LoadLevelItemDrops( static void M_LoadRoot(JSON_OBJECT *const obj, GAME_FLOW *const gf) { - const char *tmp_s; double tmp_d; JSON_ARRAY *tmp_arr; - tmp_s = JSON_ObjectGetString(obj, "savegame_fmt_bson", JSON_INVALID_STRING); - if (tmp_s == JSON_INVALID_STRING) { - Shell_ExitSystem("'savegame_fmt_bson' must be a string"); - } - gf->savegame_fmt_bson = Memory_DupStr(tmp_s); - tmp_d = JSON_ObjectGetDouble(obj, "demo_delay", -1.0); if (tmp_d < 0.0) { Shell_ExitSystem("'demo_delay' must be a positive number"); } gf->demo_delay = tmp_d; - gf->settings = m_DefaultSettings; M_LoadSettings(obj, &gf->settings); gf->enable_tr2_item_drops = diff --git a/src/libtrx/game/game_flow/reader_tr2.def.c b/src/libtrx/game/game_flow/reader_tr2.def.c index c2288cb49..88ffbbf0b 100644 --- a/src/libtrx/game/game_flow/reader_tr2.def.c +++ b/src/libtrx/game/game_flow/reader_tr2.def.c @@ -47,6 +47,8 @@ static M_SEQUENCE_EVENT_HANDLER m_SequenceEventHandlers[] = { static void M_LoadSettings( JSON_OBJECT *const obj, GF_LEVEL_SETTINGS *const settings) { + M_LoadCommonSettings(obj, settings); + { const char *tmp_s = JSON_ObjectGetString(obj, "sfx_path", JSON_INVALID_STRING); diff --git a/src/libtrx/game/game_flow/sequencer.c b/src/libtrx/game/game_flow/sequencer.c index a2fdc74c3..15eb39360 100644 --- a/src/libtrx/game/game_flow/sequencer.c +++ b/src/libtrx/game/game_flow/sequencer.c @@ -81,10 +81,6 @@ GF_COMMAND GF_InterpretSequence( const GF_SEQUENCE *const sequence = &level->sequence; for (int32_t i = 0; i < sequence->length; i++) { const GF_SEQUENCE_EVENT *const event = &sequence->events[i]; - if (GF_ShouldSkipSequenceEvent(level, event)) { - continue; - } - if (M_PostponeEvent(event)) { continue; } diff --git a/src/libtrx/game/game_flow/sequencer_events.c b/src/libtrx/game/game_flow/sequencer_events.c index 9e4e68f65..159441465 100644 --- a/src/libtrx/game/game_flow/sequencer_events.c +++ b/src/libtrx/game/game_flow/sequencer_events.c @@ -37,7 +37,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayCutscene) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const int16_t cutscene_num = (int16_t)(intptr_t)event->data; - if (seq_ctx != GFSC_SAVED) { + if (seq_ctx != GFSC_SAVED && g_Config.gameplay.enable_cutscenes) { gf_cmd = GF_DoCutsceneSequence(cutscene_num); if (gf_cmd.action == GF_LEVEL_COMPLETE) { gf_cmd.action = GF_NOOP; @@ -50,13 +50,18 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayFMV) { GF_COMMAND gf_cmd = { .action = GF_NOOP }; const int16_t fmv_id = (int16_t)(intptr_t)event->data; - if (seq_ctx != GFSC_SAVED) { - if (fmv_id < 0 || fmv_id >= g_GameFlow.fmv_count) { - LOG_ERROR("Invalid FMV number: %d", fmv_id); - } else { - FMV_Play(g_GameFlow.fmvs[fmv_id].path); - } + if (seq_ctx == GFSC_SAVED) { + return gf_cmd; } + if (fmv_id < 0 || fmv_id >= g_GameFlow.fmv_count) { + LOG_ERROR("Invalid FMV number: %d", fmv_id); + return gf_cmd; + } + const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id]; + if (fmv->is_legal && !g_Config.gameplay.enable_legal) { + return gf_cmd; + } + FMV_Play(fmv->path); return gf_cmd; } @@ -73,14 +78,11 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePicture) return gf_cmd; } -#if TR_VERSION == 1 - if (Game_GetCurrentLevel() == nullptr - && !g_Config.gameplay.enable_eidos_logo) { + GF_DISPLAY_PICTURE_DATA *data = event->data; + if (data->is_legal && !g_Config.gameplay.enable_legal) { return gf_cmd; } -#endif - GF_DISPLAY_PICTURE_DATA *data = event->data; PHASE *const phase = Phase_Picture_Create((PHASE_PICTURE_ARGS) { .file_name = data->path, .display_time = data->display_time, diff --git a/src/libtrx/game/game_flow/sequencer_priv.h b/src/libtrx/game/game_flow/sequencer_priv.h index fc02aff74..9ac810c1b 100644 --- a/src/libtrx/game/game_flow/sequencer_priv.h +++ b/src/libtrx/game/game_flow/sequencer_priv.h @@ -3,8 +3,6 @@ extern void GF_PreSequenceHook(GF_SEQUENCE_CONTEXT seq_ctx, void *seq_ctx_arg); extern GF_SEQUENCE_CONTEXT GF_SwitchSequenceContext( const GF_SEQUENCE_EVENT *event, GF_SEQUENCE_CONTEXT seq_ctx); -extern bool GF_ShouldSkipSequenceEvent( - const GF_LEVEL *level, const GF_SEQUENCE_EVENT *event); // Defer execution of certain events to run it at various stages of // GFS_LOOP_GAME. diff --git a/src/libtrx/game/gym.c b/src/libtrx/game/gym.c new file mode 100644 index 000000000..e51035d44 --- /dev/null +++ b/src/libtrx/game/gym.c @@ -0,0 +1,137 @@ +#include "game/gym.h" + +#include "config.h" +#include "game/const.h" +#include "game/game.h" +#include "game/game_flow.h" +#include "game/music.h" +#include "game/savegame.h" +#include "game/stats.h" + +#define NO_TIME (-1) + +static bool m_IsInventoryOpenEnabled = true; +static bool m_IsAssaultTimerDisplay = false; +static bool m_IsAssaultTimerActive = false; + +static int32_t M_GetBestTime(void); +static bool M_StoreAssaultTime(uint32_t time); + +static int32_t M_GetBestTime(void) +{ + const ASSAULT_STATS *const assault = &g_Config.profile.assault_stats; + return assault->total_attempts > 0 ? (int32_t)assault->entries[0].time + : NO_TIME; +} + +static bool M_StoreAssaultTime(const uint32_t time) +{ + ASSAULT_STATS *const assault = &g_Config.profile.assault_stats; + int32_t insert_idx = -1; + for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) { + if (assault->entries[i].time == 0 || time < assault->entries[i].time) { + insert_idx = i; + break; + } + } + if (insert_idx == -1) { + return false; + } + + for (int32_t i = MAX_ASSAULT_TIMES - 1; i > insert_idx; i--) { + assault->entries[i] = assault->entries[i - 1]; + } + + assault->total_attempts++; + assault->entries[insert_idx].time = time; + assault->entries[insert_idx].attempt_num = assault->total_attempts; + Config_Write(); + return true; +} + +void Gym_SetInventoryOpenEnabled(const bool enabled) +{ + m_IsInventoryOpenEnabled = enabled; +} + +bool Gym_IsInventoryOpenEnabled(void) +{ + return m_IsInventoryOpenEnabled; +} + +bool Gym_IsAssaultTimerDisplay(void) +{ + return m_IsAssaultTimerDisplay; +} + +bool Gym_IsAssaultTimerActive(void) +{ + return m_IsAssaultTimerActive; +} + +ASSAULT_STATS Gym_GetAssaultStats(void) +{ + return g_Config.profile.assault_stats; +} + +void Gym_ResetAssault(void) +{ + m_IsAssaultTimerActive = false; + m_IsAssaultTimerDisplay = false; +} + +void Gym_StartAssault(void) +{ + RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + resume->stats.timer = 0; + m_IsAssaultTimerActive = true; + m_IsAssaultTimerDisplay = true; + Stats_StartTimer(); +} + +void Gym_StopAssault(void) +{ + m_IsAssaultTimerActive = false; + m_IsAssaultTimerDisplay = true; +} + +void Gym_FinishAssault(void) +{ + if (!m_IsAssaultTimerActive) { + return; + } + + const int32_t current_best_time = M_GetBestTime(); + ASSAULT_STATS *const assault = &g_Config.profile.assault_stats; + const RESUME_INFO *const resume = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + M_StoreAssaultTime(resume->stats.timer); + + if (current_best_time <= 0) { + if (resume->stats.timer < 100 * LOGIC_FPS) { + // "Gosh! That was my best time yet!" + Music_Play(MX_GYM_HINT_15, MPM_ALWAYS); + } else { + // "Congratulations! You did it! But perhaps I could've been + // faster." + Music_Play(MX_GYM_HINT_17, MPM_ALWAYS); + } + } else if (resume->stats.timer < (uint32_t)current_best_time) { + // "Gosh! That was my best time yet!" + Music_Play(MX_GYM_HINT_15, MPM_ALWAYS); + } else if ( + resume->stats.timer < (uint32_t)current_best_time + 5 * LOGIC_FPS) { + // "Almost. Perhaps another try and I might beat it." + Music_Play(MX_GYM_HINT_16, MPM_ALWAYS); + } else { + // "Great. But nowhere near my best time." + Music_Play(MX_GYM_HINT_14, MPM_ALWAYS); + } + + m_IsAssaultTimerActive = false; +} + +bool Gym_HasAssaultStats(void) +{ + return TR_VERSION >= 2; +} diff --git a/src/libtrx/game/inject/common.c b/src/libtrx/game/inject/common.c index d7fa374e2..d66c016b3 100644 --- a/src/libtrx/game/inject/common.c +++ b/src/libtrx/game/inject/common.c @@ -180,6 +180,8 @@ static bool M_IsRelevant(const INJECTION_FILE_TYPE type) return g_Config.gameplay.fix_floor_data_issues; case IFT_ITEM_POSITION: return g_Config.visuals.fix_item_rots; + case IFT_TEXTURE_FIX: + return g_Config.visuals.fix_texture_issues; #if TR_VERSION == 1 case IFT_LARA_ANIMS: return true; @@ -187,8 +189,6 @@ static bool M_IsRelevant(const INJECTION_FILE_TYPE type) return g_Config.visuals.enable_braid; case IFT_UZI_SFX: return g_Config.audio.enable_ps_uzi_sfx; - case IFT_TEXTURE_FIX: - return g_Config.visuals.fix_texture_issues; case IFT_PS1_ENEMY: return g_Config.gameplay.restore_ps1_enemies; case IFT_DISABLE_ANIM_SPRITE: diff --git a/src/libtrx/game/inject/data/anims.c b/src/libtrx/game/inject/data/anims.c index a161af22e..79429b6ec 100644 --- a/src/libtrx/game/inject/data/anims.c +++ b/src/libtrx/game/inject/data/anims.c @@ -1,9 +1,11 @@ #include "debug.h" #include "game/anims.h" #include "game/inject.h" +#include "game/objects.h" #include "memory.h" static void M_HandleAnimData(INJECTION_CHUNK chunk); +static void M_CommandEdits(const INJECTION *injection, int32_t data_count); static void M_HandleAnimData(const INJECTION_CHUNK chunk) { @@ -90,4 +92,28 @@ static void M_HandleAnimData(const INJECTION_CHUNK chunk) } } +static void M_CommandEdits( + const INJECTION *const injection, const int32_t data_count) +{ + const LEVEL_INFO cached_info = Inject_GetCachedInfo(); + int16_t cmd_idx = cached_info.anims.command_count; + for (int32_t i = 0; i < data_count; i++) { + const GAME_OBJECT_ID obj_id = VFile_ReadS32(injection->fp); + const int32_t anim_idx = VFile_ReadS32(injection->fp); + const int32_t num_raw_cmds = VFile_ReadS32(injection->fp); + const int32_t num_anim_cmds = VFile_ReadS32(injection->fp); + + const OBJECT *const obj = Object_Get(obj_id); + if (!obj->loaded) { + continue; + } + + ANIM *const anim = Object_GetAnim(obj, anim_idx); + anim->command_idx = cmd_idx; + anim->num_commands = num_anim_cmds; + cmd_idx += num_raw_cmds; + } +} + REGISTER_INJECTOR(ICT_ANIMATION_DATA, M_HandleAnimData) +REGISTER_INJECT_EDITOR(IDT_ANIM_CMD_EDITS, M_CommandEdits) diff --git a/src/libtrx/game/inject/data/textures.c b/src/libtrx/game/inject/data/textures.c index 218bb32a9..f970ad9d9 100644 --- a/src/libtrx/game/inject/data/textures.c +++ b/src/libtrx/game/inject/data/textures.c @@ -147,7 +147,7 @@ static void M_HandleSpriteSequences( obj->mesh_count = num_meshes; obj->mesh_idx = mesh_idx + level_info->textures.sprite_count; obj->loaded = true; - } else if (obj_id - O_NUMBER_OF < MAX_STATIC_OBJECTS) { + } else if (obj_id - O_NUMBER_OF < MAX_STATIC_OBJECTS_2D) { STATIC_OBJECT_2D *const obj = Object_Get2DStatic(obj_id - O_NUMBER_OF); obj->frame_count = ABS(num_meshes); diff --git a/src/libtrx/game/inject/editors/meshes.c b/src/libtrx/game/inject/editors/meshes.c index 9544fae4b..a26fc7d7d 100644 --- a/src/libtrx/game/inject/editors/meshes.c +++ b/src/libtrx/game/inject/editors/meshes.c @@ -104,7 +104,7 @@ static void M_ApplyMeshEdit(const MESH_EDIT *const edit) } mesh = Object_GetMesh(obj->mesh_idx + edit->mesh_idx); - } else if (edit->object_id - O_NUMBER_OF < MAX_STATIC_OBJECTS) { + } else if (edit->object_id - O_NUMBER_OF < MAX_STATIC_OBJECTS_3D) { const STATIC_OBJECT_3D *const obj = Object_Get3DStatic(edit->object_id - O_NUMBER_OF); mesh = Object_GetMesh(obj->mesh_idx); diff --git a/src/libtrx/game/inject/editors/rooms.c b/src/libtrx/game/inject/editors/rooms.c index b2a62b421..2e4f02f80 100644 --- a/src/libtrx/game/inject/editors/rooms.c +++ b/src/libtrx/game/inject/editors/rooms.c @@ -299,7 +299,7 @@ static void M_RoomPortalEdits( const ROOM *const room = Room_Get(base_room); PORTAL *portal = nullptr; for (int32_t j = 0; j < room->portals->count; j++) { - PORTAL room_portal = room->portals->portal[j]; + const PORTAL room_portal = room->portals->portal[j]; if (room_portal.room_num == link_room && j == portal_index) { portal = &room->portals->portal[j]; break; @@ -314,10 +314,23 @@ static void M_RoomPortalEdits( continue; } + bool empty_portal = true; for (int32_t j = 0; j < 4; j++) { portal->vertex[j].x += VFile_ReadS16(injection->fp); portal->vertex[j].y += VFile_ReadS16(injection->fp); portal->vertex[j].z += VFile_ReadS16(injection->fp); + empty_portal &= portal->vertex[j].x == 0 && portal->vertex[j].y == 0 + && portal->vertex[j].z == 0; + } + + // An injection that has reset all vertices such that the portal size is + // now 0, should be interpreted as a command to ignore that portal. + if (empty_portal) { + for (int32_t j = portal_index + 1; j < room->portals->count; j++) { + room->portals->portal[j - 1] = room->portals->portal[j]; + } + room->portals->portal[room->portals->count - 1] = *portal; + room->portals->count--; } } } diff --git a/src/libtrx/game/input/backends/keyboard.c b/src/libtrx/game/input/backends/keyboard.c index 440b93eb8..682d1fa13 100644 --- a/src/libtrx/game/input/backends/keyboard.c +++ b/src/libtrx/game/input/backends/keyboard.c @@ -299,7 +299,8 @@ static bool M_Key(const INPUT_LAYOUT layout, const INPUT_ROLE role) return false; } #ifdef _WIN32 - if (scancode == SDL_SCANCODE_F4 && KEY_DOWN(SDL_SCANCODE_LALT)) { + if (scancode == SDL_SCANCODE_F4 + && (KEY_DOWN(SDL_SCANCODE_LALT) || KEY_DOWN(SDL_SCANCODE_RALT))) { return false; } #endif diff --git a/src/libtrx/game/input/common.c b/src/libtrx/game/input/common.c index 16ae6e33e..e4202f48f 100644 --- a/src/libtrx/game/input/common.c +++ b/src/libtrx/game/input/common.c @@ -53,10 +53,10 @@ static bool m_IsRoleHardcoded[INPUT_ROLE_NUMBER_OF] = { }; static const GAME_STRING_ID m_LayoutMap[INPUT_LAYOUT_NUMBER_OF] = { - [INPUT_LAYOUT_DEFAULT] = GS_ID(CONTROL_DEFAULT_KEYS), - [INPUT_LAYOUT_CUSTOM_1] = GS_ID(CONTROL_CUSTOM_1), - [INPUT_LAYOUT_CUSTOM_2] = GS_ID(CONTROL_CUSTOM_2), - [INPUT_LAYOUT_CUSTOM_3] = GS_ID(CONTROL_CUSTOM_3), + [INPUT_LAYOUT_DEFAULT] = GS_ID(CONTROLS_DEFAULT_KEYS), + [INPUT_LAYOUT_CUSTOM_1] = GS_ID(CONTROLS_CUSTOM_1), + [INPUT_LAYOUT_CUSTOM_2] = GS_ID(CONTROLS_CUSTOM_2), + [INPUT_LAYOUT_CUSTOM_3] = GS_ID(CONTROLS_CUSTOM_3), }; static INPUT_BACKEND_IMPL *M_GetBackend(INPUT_BACKEND backend); diff --git a/src/libtrx/game/inventory.c b/src/libtrx/game/inventory.c index 0d552545e..b3fe43546 100644 --- a/src/libtrx/game/inventory.c +++ b/src/libtrx/game/inventory.c @@ -4,6 +4,8 @@ #include "game/objects/vars.h" #include "utils.h" +INVENTORY_MODE g_Inv_Mode = INV_TITLE_MODE; + bool Inv_AddItemNTimes(const GAME_OBJECT_ID obj_id, const int32_t qty) { bool result = false; diff --git a/src/libtrx/game/items.c b/src/libtrx/game/items.c index ac9ee6329..a2698b37b 100644 --- a/src/libtrx/game/items.c +++ b/src/libtrx/game/items.c @@ -301,7 +301,11 @@ void Item_SwitchToObjAnim( const GAME_OBJECT_ID obj_id) { const OBJECT *const obj = Object_Get(obj_id); - item->anim_num = obj->anim_idx + anim_idx; + if (obj->anim_idx == NO_ANIM) { + item->anim_num = NO_ANIM; + } else { + item->anim_num = obj->anim_idx + anim_idx; + } const ANIM *const anim = Item_GetAnim(item); if (frame < 0) { @@ -506,3 +510,57 @@ void Item_PlayAnimSFX( Sound_Effect(data->effect_num, &item->pos, play_mode); } + +bool Item_TestBoundsCollide( + const ITEM *const src_item, const ITEM *const dst_item, + const int32_t radius) +{ + const BOUNDS_16 *const src_bounds = &Item_GetBestFrame(src_item)->bounds; + const BOUNDS_16 *const dst_bounds = &Item_GetBestFrame(dst_item)->bounds; + + if (src_item->pos.y + src_bounds->min.y + >= dst_item->pos.y + dst_bounds->max.y + || src_item->pos.y + src_bounds->max.y + <= dst_item->pos.y + dst_bounds->min.y) { + return false; + } + + const int32_t c = Math_Cos(src_item->rot.y); + const int32_t s = Math_Sin(src_item->rot.y); + const int32_t dx = dst_item->pos.x - src_item->pos.x; + const int32_t dz = dst_item->pos.z - src_item->pos.z; + const int32_t rx = (c * dx - s * dz) >> W2V_SHIFT; + const int32_t rz = (c * dz + s * dx) >> W2V_SHIFT; + + // clang-format off + return ( + rx >= src_bounds->min.x - radius && + rx <= src_bounds->max.x + radius && + rz >= src_bounds->min.z - radius && + rz <= src_bounds->max.z + radius); + // clang-format on +} + +bool Item_IsTriggerActive(ITEM *const item) +{ + const bool ok = !(item->flags & IF_REVERSE); + + if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) { + return !ok; + } + + if (!item->timer) { + return ok; + } + + if (item->timer == -1) { + return !ok; + } + + item->timer--; + if (item->timer == 0) { + item->timer = -1; + } + + return ok; +} diff --git a/src/libtrx/game/lara/common.c b/src/libtrx/game/lara/common.c index 8c5dd62fb..2f2274ede 100644 --- a/src/libtrx/game/lara/common.c +++ b/src/libtrx/game/lara/common.c @@ -151,3 +151,8 @@ void Lara_TakeDamage(const int16_t damage, const bool hit_status) { Item_TakeDamage(Lara_GetItem(), damage, hit_status); } + +bool Lara_TestBoundsCollide(const ITEM *const item, const int32_t radius) +{ + return Item_TestBoundsCollide(item, Lara_GetItem(), radius); +} diff --git a/src/libtrx/game/lara/misc.c b/src/libtrx/game/lara/misc.c index a1343b225..acf9a9b74 100644 --- a/src/libtrx/game/lara/misc.c +++ b/src/libtrx/game/lara/misc.c @@ -1,5 +1,6 @@ #include "game/lara/misc.h" +#include "game/items.h" #include "game/lara/common.h" #include "game/lara/const.h" #include "game/rooms.h" diff --git a/src/libtrx/game/level/common.c b/src/libtrx/game/level/common.c index cd7c346ea..cc54110f1 100644 --- a/src/libtrx/game/level/common.c +++ b/src/libtrx/game/level/common.c @@ -297,6 +297,7 @@ static void M_ReadObjectMesh(OBJECT_MESH *const mesh, VFILE *const file) VFile_Skip(file, sizeof(int16_t)); mesh->enable_reflections = false; + mesh->disable_lighting = false; { mesh->num_vertices = VFile_ReadS16(file); @@ -773,9 +774,9 @@ void Level_AppendAnimBones( const int32_t flags = VFile_ReadS32(file); bone->matrix_pop = (flags & 1) != 0; bone->matrix_push = (flags & 2) != 0; - bone->rot_x = false; - bone->rot_y = false; - bone->rot_z = false; + bone->rot.x = false; + bone->rot.y = false; + bone->rot.z = false; M_ReadPosition(&bone->pos, file); } } @@ -841,10 +842,10 @@ void Level_ReadStaticObjects(VFILE *const file) LOG_INFO("static objects: %d", num_objects); for (int32_t i = 0; i < num_objects; i++) { const int32_t static_id = VFile_ReadS32(file); - if (static_id < 0 || static_id >= MAX_STATIC_OBJECTS) { + if (static_id < 0 || static_id >= MAX_STATIC_OBJECTS_3D) { Shell_ExitSystemFmt( "Invalid static ID: %d (max=%d)", static_id, - MAX_STATIC_OBJECTS); + MAX_STATIC_OBJECTS_3D - 1); } STATIC_OBJECT_3D *const obj = Object_Get3DStatic(static_id); @@ -880,6 +881,7 @@ void Level_AppendObjectTextures( { for (int32_t i = 0; i < num_textures; i++) { OBJECT_TEXTURE *const texture = Output_GetObjectTexture(base_idx + i); + texture->uv_count = 4; // Default to 4 vertices texture->draw_type = VFile_ReadU16(file); texture->tex_page = VFile_ReadU16(file) + base_page_idx; for (int32_t j = 0; j < 4; j++) { @@ -933,8 +935,9 @@ void Level_ReadSpriteSequences(VFILE *const file) OBJECT *const obj = Object_Get(object_id); obj->mesh_count = num_meshes; obj->mesh_idx = mesh_idx; + obj->anim_idx = NO_ANIM; obj->loaded = true; - } else if (object_id - O_NUMBER_OF < MAX_STATIC_OBJECTS) { + } else if (object_id - O_NUMBER_OF < MAX_STATIC_OBJECTS_2D) { STATIC_OBJECT_2D *const obj = Object_Get2DStatic(object_id - O_NUMBER_OF); obj->frame_count = ABS(num_meshes); @@ -1187,6 +1190,26 @@ finish: void Level_LoadTextures(void) { + for (int32_t room_num = 0; room_num < Room_GetCount(); room_num++) { + const ROOM *const room = Room_Get(room_num); + for (int32_t j = 0; j < room->mesh.num_face3s; j++) { + const FACE3 *const face = &room->mesh.face3s[j]; + OBJECT_TEXTURE *const texture = + Output_GetObjectTexture(face->texture_idx); + texture->uv_count = 3; + } + } + + for (int32_t i = 0; i < Object_GetMeshCount(); i++) { + const OBJECT_MESH *const mesh = Object_GetMesh(i); + for (int32_t j = 0; j < mesh->num_tex_face3s; j++) { + const FACE3 *const face = &mesh->tex_face3s[j]; + OBJECT_TEXTURE *const texture = + Output_GetObjectTexture(face->texture_idx); + texture->uv_count = 3; + } + } + for (int32_t room_num = 0; room_num < Room_GetCount(); room_num++) { ROOM *const room = Room_Get(room_num); for (int32_t j = 0; j < room->mesh.num_face4s; j++) { diff --git a/src/libtrx/game/level/settings.c b/src/libtrx/game/level/settings.c new file mode 100644 index 000000000..7a666dca8 --- /dev/null +++ b/src/libtrx/game/level/settings.c @@ -0,0 +1,31 @@ +#include "game/level/settings.h" + +#include "config.h" +#include "game/game_flow.h" + +RGB_888 Level_GetWaterColor(void) +{ + const GF_LEVEL *const level = GF_GetCurrentLevel(); + if (level != nullptr && level->settings.water_color.is_present) { + return level->settings.water_color.value; + } + return g_Config.visuals.water_color; +} + +float Level_GetFogStart(void) +{ + const GF_LEVEL *const level = GF_GetCurrentLevel(); + if (level != nullptr && level->settings.fog_start.is_present) { + return level->settings.fog_start.value; + } + return g_Config.visuals.fog_start; +} + +float Level_GetFogEnd(void) +{ + const GF_LEVEL *const level = GF_GetCurrentLevel(); + if (level != nullptr && level->settings.fog_end.is_present) { + return level->settings.fog_end.value; + } + return g_Config.visuals.fog_end; +} diff --git a/src/libtrx/game/objects/common.c b/src/libtrx/game/objects/common.c index 54a7425d1..146485ede 100644 --- a/src/libtrx/game/objects/common.c +++ b/src/libtrx/game/objects/common.c @@ -6,13 +6,27 @@ #include "game/game_buf.h" #include "game/matrix.h" #include "game/output.h" +#include "game/output/objects.h" static OBJECT m_Objects[O_NUMBER_OF] = {}; -static STATIC_OBJECT_3D m_StaticObjects3D[MAX_STATIC_OBJECTS] = {}; -static STATIC_OBJECT_2D m_StaticObjects2D[MAX_STATIC_OBJECTS] = {}; +static STATIC_OBJECT_3D m_StaticObjects3D[MAX_STATIC_OBJECTS_3D] = {}; +static STATIC_OBJECT_2D m_StaticObjects2D[MAX_STATIC_OBJECTS_2D] = {}; static OBJECT_MESH **m_MeshPointers = nullptr; static int32_t m_MeshCount = 0; +void Object_Reset(void) +{ + for (int32_t i = 0; i < O_NUMBER_OF; i++) { + m_Objects[i].loaded = false; + } + for (int32_t i = 0; i < MAX_STATIC_OBJECTS_3D; i++) { + m_StaticObjects3D[i].loaded = false; + } + for (int32_t i = 0; i < MAX_STATIC_OBJECTS_2D; i++) { + m_StaticObjects2D[i].loaded = false; + } +} + OBJECT *Object_Get(const GAME_OBJECT_ID obj_id) { return &m_Objects[obj_id]; @@ -84,6 +98,16 @@ OBJECT_MESH *Object_GetMesh(const int32_t index) return m_MeshPointers[index]; } +int32_t Object_GetMeshIndex(const OBJECT_MESH *const mesh) +{ + for (int32_t i = 0; i < m_MeshCount; i++) { + if (mesh == m_MeshPointers[i]) { + return i; + } + } + return -1; +} + int32_t Object_GetMeshCount(void) { return m_MeshCount; @@ -122,6 +146,12 @@ void Object_SwapMesh( m_MeshPointers[obj1->mesh_idx + mesh_num] = m_MeshPointers[obj2->mesh_idx + mesh_num]; m_MeshPointers[obj2->mesh_idx + mesh_num] = temp; + +#if TR_VERSION == 1 + Output_Meshes_ObserveObjectMeshSwap( + m_MeshPointers[obj1->mesh_idx + mesh_num], + m_MeshPointers[obj2->mesh_idx + mesh_num]); +#endif } ANIM *Object_GetAnim(const OBJECT *const obj, const int32_t anim_idx) @@ -155,6 +185,7 @@ void Object_DrawInterpolatedObject( Matrix_TranslateRel16_ID(frame1->offset, frame2->offset); Matrix_Rot16_ID( frame1->mesh_rots[mesh_idx], frame2->mesh_rots[mesh_idx]); + Object_ApplyExtraRotation(&extra_rotation, obj->base_rot, true); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { @@ -167,17 +198,7 @@ void Object_DrawInterpolatedObject( Matrix_TranslateRel32_I(bone->pos); Matrix_Rot16_ID( frame1->mesh_rots[mesh_idx], frame2->mesh_rots[mesh_idx]); - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY_I(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX_I(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ_I(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, true); } if (meshes & (1 << mesh_idx)) { @@ -189,6 +210,8 @@ void Object_DrawInterpolatedObject( if (mesh_idx == 0) { Matrix_TranslateRel16(frame1->offset); Matrix_Rot16(frame1->mesh_rots[mesh_idx]); + Object_ApplyExtraRotation( + &extra_rotation, obj->base_rot, false); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { @@ -200,17 +223,7 @@ void Object_DrawInterpolatedObject( Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frame1->mesh_rots[mesh_idx]); - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); } if (meshes & (1 << mesh_idx)) { @@ -221,3 +234,29 @@ void Object_DrawInterpolatedObject( Matrix_Pop(); } + +void Object_ApplyExtraRotation( + const int16_t **extra_rotation, const XYZ_BOOL rot_flags, + const bool interpolated) +{ + const int16_t *rot_ptr = *extra_rotation; + if (rot_ptr == nullptr) { + return; + } + +#define APPLY_ROTATION(axis_, flag_) \ + if (rot_flags.flag_) { \ + if (interpolated) { \ + Matrix_Rot##axis_##_I(*rot_ptr++); \ + } else { \ + Matrix_Rot##axis_(*rot_ptr++); \ + } \ + } + + APPLY_ROTATION(Y, y); + APPLY_ROTATION(X, x); + APPLY_ROTATION(Z, z); + +#undef APPLY_ROTATION + *extra_rotation = rot_ptr; +} diff --git a/src/libtrx/game/objects/creatures/bear.c b/src/libtrx/game/objects/creatures/bear.c index dcb3b3348..6489c1d9c 100644 --- a/src/libtrx/game/objects/creatures/bear.c +++ b/src/libtrx/game/objects/creatures/bear.c @@ -1,6 +1,7 @@ #include "game/objects/creatures/bear.h" #include "config.h" +#include "game/const.h" #include "game/creature.h" #include "game/lara/common.h" #include "game/random.h" @@ -47,11 +48,7 @@ typedef enum { // clang-format on } BEAR_STATE; -#if TR_VERSION == 1 -static BITE m_BearHeadBite = { 0, 96, 335, 14 }; -#else static BITE m_BearHeadBite = { .pos = { 0, 96, 335 }, .mesh_num = 14 }; -#endif static void M_Control(int16_t item_num); @@ -246,7 +243,7 @@ void Bear_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 13)->rot_y = true; + Object_GetBone(obj, 13)->rot.y = true; } REGISTER_OBJECT(O_BEAR, Bear_Setup) diff --git a/src/libtrx/game/objects/creatures/wolf.c b/src/libtrx/game/objects/creatures/wolf.c index 138c6468b..7d1baa62b 100644 --- a/src/libtrx/game/objects/creatures/wolf.c +++ b/src/libtrx/game/objects/creatures/wolf.c @@ -1,5 +1,6 @@ #include "game/objects/creatures/wolf.h" +#include "game/const.h" #include "game/creature.h" #include "game/lara/common.h" #include "game/random.h" @@ -51,11 +52,7 @@ typedef enum { WOLF_ANIM_DEATH = 20, } WOLF_ANIM; -#if TR_VERSION == 1 -static BITE m_WolfJawBite = { 0, -14, 174, 6 }; -#else static BITE m_WolfJawBite = { .pos = { 0, -14, 174 }, .mesh_num = 6 }; -#endif static void M_Initialise(int16_t item_num); static void M_Control(int16_t item_num); @@ -234,7 +231,7 @@ void Wolf_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 2)->rot_y = true; + Object_GetBone(obj, 2)->rot.y = true; } REGISTER_OBJECT(O_WOLF, Wolf_Setup) diff --git a/src/tr1/game/objects/general/bridge_common.c b/src/libtrx/game/objects/general/bridge_common.c similarity index 86% rename from src/tr1/game/objects/general/bridge_common.c rename to src/libtrx/game/objects/general/bridge_common.c index 6d22c4096..332caf1d8 100644 --- a/src/tr1/game/objects/general/bridge_common.c +++ b/src/libtrx/game/objects/general/bridge_common.c @@ -1,18 +1,16 @@ #include "game/objects/general/bridge_common.h" -#include "game/items.h" -#include "game/room.h" -#include "global/const.h" +#include "config.h" +#include "game/rooms.h" +#include "utils.h" -#include -#include - -bool Bridge_IsSameSector(int32_t x, int32_t z, const ITEM *item) +bool Bridge_IsSameSector( + const int32_t x, const int32_t z, const ITEM *const item) { - int32_t sector_x = x / WALL_L; - int32_t sector_z = z / WALL_L; - int32_t item_sector_x = item->pos.x / WALL_L; - int32_t item_sector_z = item->pos.z / WALL_L; + const int32_t sector_x = x / WALL_L; + const int32_t sector_z = z / WALL_L; + const int32_t item_sector_x = item->pos.x / WALL_L; + const int32_t item_sector_z = item->pos.z / WALL_L; return sector_x == item_sector_x && sector_z == item_sector_z; } @@ -74,9 +72,9 @@ void Bridge_FixEmbeddedPosition(int16_t item_num) // and moves them up. ITEM *const item = Item_Get(item_num); - int32_t x = item->pos.x; - int32_t y = item->pos.y; - int32_t z = item->pos.z; + const int32_t x = item->pos.x; + const int32_t y = item->pos.y; + const int32_t z = item->pos.z; int16_t room_num = item->room_num; const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); diff --git a/src/tr1/game/objects/general/bridge_flat.c b/src/libtrx/game/objects/general/bridge_flat.c similarity index 95% rename from src/tr1/game/objects/general/bridge_flat.c rename to src/libtrx/game/objects/general/bridge_flat.c index 2044e9b70..f80424ce5 100644 --- a/src/tr1/game/objects/general/bridge_flat.c +++ b/src/libtrx/game/objects/general/bridge_flat.c @@ -1,7 +1,8 @@ +#include "config.h" +#include "game/const.h" +#include "game/objects.h" #include "game/objects/general/bridge_common.h" -#include - static int16_t M_GetFloorHeight( const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); static int16_t M_GetCeilingHeight( diff --git a/src/tr1/game/objects/general/bridge_tilt1.c b/src/libtrx/game/objects/general/bridge_tilt1.c similarity index 96% rename from src/tr1/game/objects/general/bridge_tilt1.c rename to src/libtrx/game/objects/general/bridge_tilt1.c index 2d8c84d32..77df0826f 100644 --- a/src/tr1/game/objects/general/bridge_tilt1.c +++ b/src/libtrx/game/objects/general/bridge_tilt1.c @@ -1,7 +1,8 @@ +#include "config.h" +#include "game/const.h" +#include "game/objects.h" #include "game/objects/general/bridge_common.h" -#include - static int16_t M_GetFloorHeight( const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); static int16_t M_GetCeilingHeight( diff --git a/src/tr1/game/objects/general/bridge_tilt2.c b/src/libtrx/game/objects/general/bridge_tilt2.c similarity index 96% rename from src/tr1/game/objects/general/bridge_tilt2.c rename to src/libtrx/game/objects/general/bridge_tilt2.c index 58a5b651f..c071e80a6 100644 --- a/src/tr1/game/objects/general/bridge_tilt2.c +++ b/src/libtrx/game/objects/general/bridge_tilt2.c @@ -1,7 +1,8 @@ +#include "config.h" +#include "game/const.h" +#include "game/objects.h" #include "game/objects/general/bridge_common.h" -#include - static int16_t M_GetFloorHeight( const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); static int16_t M_GetCeilingHeight( diff --git a/src/tr1/game/objects/general/drawbridge.c b/src/libtrx/game/objects/general/drawbridge.c similarity index 97% rename from src/tr1/game/objects/general/drawbridge.c rename to src/libtrx/game/objects/general/drawbridge.c index 94b45c92c..882213c49 100644 --- a/src/tr1/game/objects/general/drawbridge.c +++ b/src/libtrx/game/objects/general/drawbridge.c @@ -1,8 +1,8 @@ +#include "config.h" #include "game/items.h" +#include "game/objects.h" #include "game/objects/general/door.h" -#include "game/room.h" - -#include +#include "game/rooms.h" typedef enum { DRAWBRIDGE_STATE_CLOSED = DOOR_STATE_CLOSED, diff --git a/src/tr2/game/objects/general/trapdoor.c b/src/libtrx/game/objects/general/trapdoor.c similarity index 98% rename from src/tr2/game/objects/general/trapdoor.c rename to src/libtrx/game/objects/general/trapdoor.c index 20082f048..d9b0ee8f0 100644 --- a/src/tr2/game/objects/general/trapdoor.c +++ b/src/libtrx/game/objects/general/trapdoor.c @@ -1,4 +1,5 @@ -#include "game/items.h" +#include "game/const.h" +#include "game/objects.h" typedef enum { TRAPDOOR_STATE_CLOSED, diff --git a/src/libtrx/game/objects/traps/movable_block.c b/src/libtrx/game/objects/traps/movable_block.c index f463bb51a..155bbd485 100644 --- a/src/libtrx/game/objects/traps/movable_block.c +++ b/src/libtrx/game/objects/traps/movable_block.c @@ -7,6 +7,11 @@ #include +typedef struct { + int16_t counter_rot[3]; + int16_t original_rot; +} M_PRIV; + static int32_t m_BlockCount = 0; static VECTOR *m_UnsortedBlocks = nullptr; static int16_t *m_SortedBlocks = nullptr; @@ -54,6 +59,24 @@ void MovableBlock_Initialise(const int16_t item_num) m_UnsortedBlocks = Vector_Create(sizeof(int16_t)); } Vector_Add(m_UnsortedBlocks, (void *)&item_num); + + // Ensure the block is snapped to the grid, otherwise the snapping occurs + // during collision tests and can appear jarring. Additional angles are + // stored to preserve item appearance in spite of control angle changes. + ITEM *const item = Item_Get(item_num); + M_PRIV *const data = GameBuf_Alloc(sizeof(M_PRIV), GBUF_ITEM_DATA); + item->data = data; + data->original_rot = + (((item->rot.y + DEG_180) / DEG_90) * DEG_90) - DEG_180; + MovableBlock_UpdateRotation(item, data->original_rot); +} + +// TODO: make private +void MovableBlock_UpdateRotation(ITEM *const item, const int16_t rot_y) +{ + item->rot.y = rot_y; + M_PRIV *const data = (M_PRIV *)item->data; + data->counter_rot[0] = data->original_rot - rot_y; } void MovableBlock_SetupFloor(void) diff --git a/src/libtrx/game/output/background.c b/src/libtrx/game/output/background.c new file mode 100644 index 000000000..855047d01 --- /dev/null +++ b/src/libtrx/game/output/background.c @@ -0,0 +1,209 @@ +#include "game/output/background.h" + +#include "debug.h" +#include "filesystem.h" +#include "game/viewport.h" +#include "log.h" +#include "memory.h" +#include "strings.h" +#include "utils.h" +#include "vector.h" + +#include +#include +#include + +#define M_RELATIVE_ERROR(a, b) ABS((a) - (b)) / (b) + +typedef struct { + char *file_name; + float diff; +} M_CANDIDATE; + +static char *m_LastPath = nullptr; + +static IMAGE *M_CreateImageFromPath(const char *path); +static float M_GetScreenAspectRatio(void); +static int M_CompareCandidates(const void *a, const void *b); +static VECTOR *M_ScanCandidates(const char *path, size_t *dir_len_out); +static bool M_TryLoadCandidates( + const char *path, VECTOR *candidates, size_t dir_len); +static void M_FreeCandidates(VECTOR *candidates); + +static IMAGE *M_CreateImageFromPath(const char *const path) +{ + if (TR_VERSION == 1) { + return Image_CreateFromFileInto( + path, Viewport_GetWidth(), Viewport_GetHeight(), IMAGE_FIT_SMART); + } else { + return Image_CreateFromFile(path); + } +} + +static float M_GetScreenAspectRatio(void) +{ + return Viewport_GetWidth() / (float)Viewport_GetHeight(); +} + +static int M_CompareCandidates(const void *const a, const void *const b) +{ + const M_CANDIDATE *const c1 = a; + const M_CANDIDATE *const c2 = b; + if (c1->diff < c2->diff) { + return -1; + } + if (c1->diff > c2->diff) { + return 1; + } + return 0; +} + +static VECTOR *M_ScanCandidates( + const char *const path, size_t *const dir_len_out) +{ + VECTOR *candidates = nullptr; + + const char *last_slash = strrchr(path, '/'); + const char *last_backslash = strrchr(path, '\\'); + const char *last_sep = + last_slash > last_backslash ? last_slash : last_backslash; + size_t dir_len; + char *dir_path; + if (last_sep != nullptr) { + dir_len = last_sep - path; + dir_path = String_Format("%.*s", (int)dir_len, path); + } else { + dir_len = 0; + dir_path = Memory_DupStr("."); + } + + const char *file_name = last_sep ? last_sep + 1 : path; + const char *ext_ptr = strrchr(file_name, '.'); + const float screen_ratio = M_GetScreenAspectRatio(); + + void *const dir_handle = File_OpenDirectory(dir_path); + if (dir_handle == nullptr) { + goto finish; + } + + candidates = Vector_Create(sizeof(M_CANDIDATE)); + const char *entry; + while ((entry = File_ReadDirectory(dir_handle)) != nullptr) { + // Match the file itself, and assume it's of 16:9 aspect ratio. + if (String_Equivalent(entry, file_name)) { + const float ratio = 16.0f / 9.0f; + Vector_Add( + candidates, + &(M_CANDIDATE) { + .file_name = Memory_DupStr(file_name), + .diff = M_RELATIVE_ERROR(ratio, screen_ratio), + }); + } + + // Match directories with pattern: x + int32_t w = 0, h = 0; + if (sscanf(entry, "%dx%d", &w, &h) == 2) { + const float ratio = w / (float)h; + Vector_Add( + candidates, + &(M_CANDIDATE) { + .file_name = String_Format("%s/%s", entry, file_name), + .diff = M_RELATIVE_ERROR(ratio, screen_ratio), + }); + } + } + File_CloseDirectory(dir_handle); + +finish: + *dir_len_out = dir_len; + Memory_FreePointer(&dir_path); + return candidates; +} + +static bool M_TryLoadCandidates( + const char *const path, VECTOR *const candidates, const size_t dir_len) +{ + for (int32_t i = 0; i < candidates->count; i++) { + const M_CANDIDATE *candidate = Vector_Get(candidates, i); + char *full_path; + if (dir_len > 0) { + full_path = String_Format( + "%.*s/%s", (int32_t)dir_len, path, candidate->file_name); + } else { + full_path = String_Format("%s", candidate->file_name); + } + IMAGE *const image = M_CreateImageFromPath(full_path); + Memory_FreePointer(&full_path); + if (image != nullptr) { + Output_LoadBackgroundFromImage(image); + Image_Free(image); + return true; + } + } + return false; +} + +static void M_FreeCandidates(VECTOR *const candidates) +{ + if (candidates == nullptr) { + return; + } + for (int32_t i = 0; i < candidates->count; i++) { + M_CANDIDATE *const candidate = Vector_Get(candidates, i); + Memory_Free(candidate->file_name); + } + Vector_Free(candidates); +} + +bool Output_LoadBackgroundFromFile(const char *const path) +{ + LOG_INFO("Loading image %s", path); + size_t dir_len = 0; + bool result = false; + + // Try aspect-ratio specific directories. + VECTOR *candidates = M_ScanCandidates(path, &dir_len); + if (candidates != nullptr) { + M_CANDIDATE *raw_candidates = Vector_GetData(candidates); + qsort( + raw_candidates, candidates->count, sizeof(M_CANDIDATE), + M_CompareCandidates); + for (int32_t i = 0; i < candidates->count; i++) { + const M_CANDIDATE *const candidate = Vector_Get(candidates, i); + LOG_INFO( + "Found candidate %s (diff=%.02f)", candidate->file_name, + candidate->diff); + } + if (M_TryLoadCandidates(path, candidates, dir_len)) { + result = true; + } + } + + if (!result) { + // Fallback to the main image. + IMAGE *const image = M_CreateImageFromPath(path); + if (image != nullptr) { + result = true; + Output_LoadBackgroundFromImage(image); + Image_Free(image); + } + } + + M_FreeCandidates(candidates); + if (result) { + char *prev = m_LastPath; + m_LastPath = Memory_DupStr(path); + Memory_FreePointer(&prev); + } + return result; +} + +char *Output_GetLastBackgroundPath(void) +{ + return m_LastPath; +} + +void Output_ClearLastBackgroundPath(void) +{ + Memory_FreePointer(&m_LastPath); +} diff --git a/src/libtrx/game/output/common.c b/src/libtrx/game/output/common.c index 7a6c9ed01..c8a835fce 100644 --- a/src/libtrx/game/output/common.c +++ b/src/libtrx/game/output/common.c @@ -10,6 +10,7 @@ typedef struct { int32_t shade; } COMMON_LIGHT; +static int32_t m_FogStart = 0; static int32_t m_DynamicLightCount = 0; static LIGHT m_DynamicLights[MAX_DYNAMIC_LIGHTS] = {}; @@ -51,7 +52,7 @@ static void M_CalculateBrightestLight( } #endif - const int32_t ambient = TR_VERSION == 1 ? (0x1FFF - room->ambient) : 0; + const int32_t ambient = TR_VERSION == 1 ? (SHADE_MAX - room->ambient) : 0; for (int32_t i = 0; i < room->num_lights; i++) { const LIGHT *const light = &room->lights[i]; const int32_t dx = pos.x - light->pos.x; @@ -111,7 +112,7 @@ void Output_CalculateLight(const XYZ_32 pos, const int16_t room_num) adder = (adder + dynamic_adder) / 2; if (TR_VERSION == 1 && (room->num_lights > 0 || dynamic_adder > 0)) { - adder += (0x1FFF - room->ambient) / 2; + adder += (SHADE_MAX - room->ambient) / 2; } // TODO: use m_LsAdder and m_LsDivider once ported @@ -122,7 +123,7 @@ void Output_CalculateLight(const XYZ_32 pos, const int16_t room_num) global_divider = 0; } else { #if TR_VERSION == 1 - global_adder = 0x1FFF - adder; + global_adder = SHADE_MAX - adder; const int32_t divider = brightest_light.shade == adder ? adder : brightest_light.shade - adder; @@ -140,7 +141,7 @@ void Output_CalculateLight(const XYZ_32 pos, const int16_t room_num) const int32_t depth = g_MatrixPtr->_23 >> W2V_SHIFT; global_adder += Output_CalcFogShade(depth); - CLAMPG(global_adder, 0x1FFF); + CLAMPG(global_adder, SHADE_MAX); Output_SetLightAdder(global_adder); Output_SetLightDivider(global_divider); @@ -149,10 +150,10 @@ void Output_CalculateLight(const XYZ_32 pos, const int16_t room_num) void Output_CalculateStaticLight(const int16_t adder) { // TODO: use m_LsAdder - int32_t global_adder = adder - 0x1000; + int32_t global_adder = adder - SHADE_NEUTRAL; const int32_t depth = g_MatrixPtr->_23 >> W2V_SHIFT; global_adder += Output_CalcFogShade(depth); - CLAMPG(global_adder, 0x1FFF); + CLAMPG(global_adder, SHADE_MAX); Output_SetLightAdder(global_adder); } @@ -294,3 +295,13 @@ void Output_AddDynamicLight( light->shade.value_1 = intensity; light->falloff.value_1 = falloff; } + +int32_t Output_GetFogStart(void) +{ + return MIN(m_FogStart, Output_GetFogEnd()); +} + +void Output_SetFogStart(const int32_t dist) +{ + m_FogStart = dist; +} diff --git a/src/libtrx/game/output/textures.c b/src/libtrx/game/output/textures.c index 886efecd4..01b78f7ac 100644 --- a/src/libtrx/game/output/textures.c +++ b/src/libtrx/game/output/textures.c @@ -216,7 +216,7 @@ void Output_CycleAnimatedTextures(void) m_ObjectTextures[range->textures[i]] = temp; } - for (int32_t i = 0; i < MAX_STATIC_OBJECTS; i++) { + for (int32_t i = 0; i < MAX_STATIC_OBJECTS_2D; i++) { const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i); if (!obj->loaded || obj->frame_count == 1) { continue; diff --git a/src/libtrx/game/phase/executor.c b/src/libtrx/game/phase/executor.c index 42754ffba..8afd3f9cd 100644 --- a/src/libtrx/game/phase/executor.c +++ b/src/libtrx/game/phase/executor.c @@ -12,6 +12,7 @@ #include "game/savegame.h" #include "game/shell.h" #include "game/text.h" +#include "game/ui.h" #include "gfx/gl/track.h" #define DEBUG_OPTIM 0 @@ -28,6 +29,8 @@ static int32_t M_Wait(PHASE *phase); static PHASE_CONTROL M_Control(PHASE *const phase, const int32_t nframes) { + Console_Control(); + const GF_COMMAND gf_override_cmd = GF_GetOverrideCommand(); if (gf_override_cmd.action != GF_NOOP) { const GF_COMMAND gf_cmd = gf_override_cmd; @@ -64,6 +67,7 @@ static PHASE_CONTROL M_Control(PHASE *const phase, const int32_t nframes) static void M_Draw(PHASE *const phase) { Output_BeginScene(); + UI_BeginScene(); #if DEBUG_OPTIM BENCHMARK benchmark = Benchmark_Start(); #endif @@ -73,6 +77,7 @@ static void M_Draw(PHASE *const phase) Console_Draw(); Text_Draw(); + UI_EndScene(); Output_DrawPolyList(); Fader_Draw(&m_ExitFader); diff --git a/src/libtrx/game/phase/phase_pause.c b/src/libtrx/game/phase/phase_pause.c index f6151268b..94799b89a 100644 --- a/src/libtrx/game/phase/phase_pause.c +++ b/src/libtrx/game/phase/phase_pause.c @@ -11,8 +11,7 @@ #include "game/shell.h" #include "game/sound.h" #include "game/text.h" -#include "game/ui/common.h" -#include "game/ui/widgets/requester.h" +#include "game/ui.h" #include "memory.h" #include @@ -23,14 +22,15 @@ typedef enum { STATE_FADE_IN, STATE_WAIT, STATE_ASK, - STATE_CONFIRM, STATE_FADE_OUT, } STATE; typedef struct { STATE state; - bool is_ui_ready; - UI_WIDGET *ui; + struct { + bool is_ready; + UI_PAUSE_STATE state; + } ui; TEXTSTRING *mode_text; GF_ACTION action; FADER back_fader; @@ -43,8 +43,6 @@ static void M_ReturnToGame(M_PRIV *p); static void M_ExitToTitle(M_PRIV *p); static void M_CreateText(M_PRIV *p); static void M_RemoveText(M_PRIV *p); -static int32_t M_DisplayRequester( - M_PRIV *p, const char *header, const char *option1, const char *option2); static PHASE_CONTROL M_Start(PHASE *phase); static void M_End(PHASE *phase); @@ -60,10 +58,7 @@ static void M_FadeIn(M_PRIV *const p) static void M_FadeOut(M_PRIV *const p) { M_RemoveText(p); - if (p->ui != nullptr) { - p->ui->free(p->ui); - p->ui = nullptr; - } + p->ui.is_ready = false; if (p->action == GF_NOOP) { Fader_Init(&p->back_fader, FADER_ANY, FADER_TRANSPARENT, FADE_TIME); } else { @@ -108,42 +103,13 @@ static void M_RemoveText(M_PRIV *const p) p->mode_text = nullptr; } -static int32_t M_DisplayRequester( - M_PRIV *const p, const char *header, const char *option1, - const char *option2) -{ - if (!p->is_ui_ready) { - if (p->ui == nullptr) { - p->ui = UI_Requester_Create((UI_REQUESTER_SETTINGS) { - .is_selectable = true, - .width = 160, - .visible_rows = 2, - }); - } - UI_Requester_ClearRows(p->ui); - UI_Requester_SetTitle(p->ui, header); - UI_Requester_AddRowC(p->ui, option1, nullptr); - UI_Requester_AddRowC(p->ui, option2, nullptr); - p->ui->set_position( - p->ui, (UI_GetCanvasWidth() - p->ui->get_width(p->ui)) / 2, - (UI_GetCanvasHeight() - p->ui->get_height(p->ui)) - 50); - p->is_ui_ready = true; - } - - const int32_t choice = UI_Requester_GetSelectedRow(p->ui); - if (choice >= 0) { - p->is_ui_ready = false; - } - return choice; -} - static PHASE_CONTROL M_Start(PHASE *const phase) { M_PRIV *const p = phase->priv; + p->ui.is_ready = false; + UI_Pause_Init(&p->ui.state); M_PauseGame(p); - - p->is_ui_ready = false; return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } @@ -151,10 +117,7 @@ static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; M_RemoveText(p); - if (p->ui != nullptr) { - p->ui->free(p->ui); - p->ui = nullptr; - } + UI_Pause_Free(&p->ui.state); } static PHASE_CONTROL M_Control(PHASE *const phase, int32_t const num_frames) @@ -164,8 +127,8 @@ static PHASE_CONTROL M_Control(PHASE *const phase, int32_t const num_frames) Input_Update(); Shell_ProcessInput(); - if (p->ui != nullptr) { - p->ui->control(p->ui); + if (p->ui.is_ready) { + UI_Pause_Control(&p->ui.state); } switch (p->state) { @@ -190,30 +153,23 @@ static PHASE_CONTROL M_Control(PHASE *const phase, int32_t const num_frames) break; case STATE_ASK: { - const int32_t choice = M_DisplayRequester( - p, GS(PAUSE_EXIT_TO_TITLE), GS(PAUSE_CONTINUE), GS(PAUSE_QUIT)); - if (choice == 0) { + const UI_PAUSE_EXIT_CHOICE choice = UI_Pause_Control(&p->ui.state); + switch (choice) { + case UI_PAUSE_RESUME_PAUSE: + p->state = STATE_WAIT; + return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; + case UI_PAUSE_EXIT_TO_GAME: M_ReturnToGame(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; - } else if (choice == 1) { - p->state = STATE_CONFIRM; + case UI_PAUSE_EXIT_TO_TITLE: + M_ExitToTitle(p); return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; + default: + break; } break; } - case STATE_CONFIRM: { - const int32_t choice = M_DisplayRequester( - p, GS(PAUSE_ARE_YOU_SURE), GS(PAUSE_YES), GS(PAUSE_NO)); - if (choice == 0) { - M_ExitToTitle(p); - return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; - } else if (choice == 1) { - M_ReturnToGame(p); - return (PHASE_CONTROL) { .action = PHASE_ACTION_NO_WAIT }; - } - break; - case STATE_FADE_OUT: if (!Fader_IsActive(&p->back_fader)) { return (PHASE_CONTROL) { @@ -223,7 +179,6 @@ static PHASE_CONTROL M_Control(PHASE *const phase, int32_t const num_frames) } break; } - } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } @@ -237,8 +192,8 @@ static void M_Draw(PHASE *const phase) Interpolation_Enable(); Fader_Draw(&p->back_fader); - if (p->ui != nullptr) { - p->ui->draw(p->ui); + if (p->state == STATE_ASK) { + UI_Pause(&p->ui.state); } Output_DrawPolyList(); } diff --git a/src/libtrx/game/phase/phase_photo_mode.c b/src/libtrx/game/phase/phase_photo_mode.c index 258f53d99..d031a7b92 100644 --- a/src/libtrx/game/phase/phase_photo_mode.c +++ b/src/libtrx/game/phase/phase_photo_mode.c @@ -12,12 +12,10 @@ #include "game/overlay.h" #include "game/shell.h" #include "game/sound.h" -#include "game/ui/common.h" -#include "game/ui/widgets/photo_mode.h" +#include "game/ui.h" #include "memory.h" typedef struct { - UI_WIDGET *ui; bool taking_screenshot; bool show_fps_counter; } M_PRIV; @@ -40,7 +38,6 @@ static PHASE_CONTROL M_Start(PHASE *const phase) Music_Pause(); Sound_PauseAll(); - p->ui = UI_PhotoMode_Create(); if (!g_Config.ui.enable_photo_mode_ui) { Console_Log( GS(OSD_PHOTO_MODE_LAUNCHED), @@ -56,9 +53,6 @@ static void M_End(PHASE *const phase) M_PRIV *const p = phase->priv; Camera_ExitPhotoMode(); - p->ui->free(p->ui); - p->ui = nullptr; - #if TR_VERSION == 1 g_Config.rendering.enable_fps_counter = p->show_fps_counter; #endif @@ -92,7 +86,6 @@ static PHASE_CONTROL M_Control(PHASE *const phase, int32_t num_frames) Output_EndScene(); Sound_Effect(SFX_MENU_LARA_HOME, nullptr, SPM_ALWAYS); } else { - p->ui->control(p->ui); Camera_Update(); } @@ -106,7 +99,7 @@ static void M_Draw(PHASE *const phase) Output_DrawPolyList(); if (!p->taking_screenshot) { - p->ui->draw(p->ui); + UI_PhotoMode(); } Output_DrawPolyList(); } diff --git a/src/libtrx/game/phase/phase_stats.c b/src/libtrx/game/phase/phase_stats.c index 0fde57e95..8cda06426 100644 --- a/src/libtrx/game/phase/phase_stats.c +++ b/src/libtrx/game/phase/phase_stats.c @@ -9,7 +9,7 @@ #include "game/interpolation.h" #include "game/shell.h" #include "game/text.h" -#include "game/ui/widgets/stats_dialog.h" +#include "game/ui.h" #include "memory.h" typedef enum { @@ -24,7 +24,8 @@ typedef struct { STATE state; FADER back_fader; FADER top_fader; - UI_WIDGET *ui; + bool ui_active; + UI_STATS_DIALOG_STATE ui_state; } M_PRIV; static bool M_IsFading(M_PRIV *p); @@ -65,8 +66,11 @@ static PHASE_CONTROL M_Start(PHASE *const phase) M_PRIV *const p = phase->priv; if (p->args.background_type == BK_IMAGE) { - ASSERT(p->args.background_path != nullptr); - Output_LoadBackgroundFromFile(p->args.background_path); + if (p->args.background_path == nullptr) { + LOG_WARNING("Trying to load empty background image"); + } else { + Output_LoadBackgroundFromFile(p->args.background_path); + } } else if (p->args.background_type == BK_OBJECT) { Output_LoadBackgroundFromObject(); } else { @@ -83,14 +87,19 @@ static PHASE_CONTROL M_Start(PHASE *const phase) M_FadeIn(p); } - p->ui = UI_StatsDialog_Create((UI_STATS_DIALOG_ARGS) { - .mode = p->args.show_final_stats ? UI_STATS_DIALOG_MODE_FINAL - : UI_STATS_DIALOG_MODE_LEVEL, - .style = p->args.use_bare_style ? UI_STATS_DIALOG_STYLE_BARE - : UI_STATS_DIALOG_STYLE_BORDERED, - .level_num = p->args.level_num != -1 ? p->args.level_num - : Game_GetCurrentLevel()->num, - }); + p->ui_active = true; + UI_StatsDialog_Init( + &p->ui_state, + (UI_STATS_DIALOG_ARGS) { + .mode = p->args.show_final_stats ? UI_STATS_DIALOG_MODE_FINAL + : UI_STATS_DIALOG_MODE_LEVEL, + .style = p->args.use_bare_style + ? UI_STATS_DIALOG_STYLE_BARE + : UI_STATS_DIALOG_STYLE_BORDERED, + .level_num = p->args.level_num != -1 + ? p->args.level_num + : Game_GetCurrentLevel()->num, + }); } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; @@ -99,10 +108,11 @@ static PHASE_CONTROL M_Start(PHASE *const phase) static void M_End(PHASE *const phase) { M_PRIV *const p = phase->priv; - Output_UnloadBackground(); - if (p->ui != nullptr) { - p->ui->free(p->ui); + if (p->ui_active) { + p->ui_active = false; + UI_StatsDialog_Free(&p->ui_state); } + Output_UnloadBackground(); } static PHASE_CONTROL M_Control(PHASE *const phase, int32_t num_frames) @@ -140,9 +150,6 @@ static PHASE_CONTROL M_Control(PHASE *const phase, int32_t num_frames) }; } - if (p->ui != nullptr) { - p->ui->control(p->ui); - } return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE }; } @@ -159,13 +166,11 @@ static void M_Draw(PHASE *const phase) } Fader_Draw(&p->back_fader); - if (p->ui != nullptr) { - p->ui->draw(p->ui); + UI_BeginFade(&p->top_fader, true); + if (p->ui_active) { + UI_StatsDialog(&p->ui_state); } - Text_Draw(); - Output_DrawPolyList(); - - Fader_Draw(&p->top_fader); + UI_EndFade(); } PHASE *Phase_Stats_Create(const PHASE_STATS_ARGS args) diff --git a/src/libtrx/game/rooms/common.c b/src/libtrx/game/rooms/common.c index bea5454fa..b10c7ceb7 100644 --- a/src/libtrx/game/rooms/common.c +++ b/src/libtrx/game/rooms/common.c @@ -7,6 +7,7 @@ #include "game/items.h" #include "game/objects/common.h" #include "game/objects/traps/movable_block.h" +#include "game/output.h" #include "game/rooms/const.h" #include "game/rooms/enum.h" #include "game/sound/common.h" @@ -42,9 +43,10 @@ static const int16_t *M_ReadTrigger( const int16_t *data, int16_t fd_entry, SECTOR *sector); static void M_AddFlipItems(const ROOM *room); static void M_RemoveFlipItems(const ROOM *room); -static int16_t M_GetFloorTiltHeight(const SECTOR *sector, int32_t x, int32_t z); +static int16_t M_GetFloorTiltHeight( + const SECTOR *sector, int32_t x, int32_t z, bool fix_tilts); static int16_t M_GetCeilingTiltHeight( - const SECTOR *sector, int32_t x, int32_t z); + const SECTOR *sector, int32_t x, int32_t z, bool fix_tilts); static const int16_t *M_ReadTrigger( const int16_t *data, const int16_t fd_entry, SECTOR *const sector) @@ -154,10 +156,11 @@ static void M_RemoveFlipItems(const ROOM *const room) } static int16_t M_GetFloorTiltHeight( - const SECTOR *const sector, const int32_t x, const int32_t z) + const SECTOR *const sector, const int32_t x, const int32_t z, + const bool fix_tilts) { int16_t height = sector->floor.height; - if (sector->floor.tilt == 0) { + if (sector->floor.tilt == 0 || (height == NO_HEIGHT && fix_tilts)) { return height; } @@ -188,10 +191,11 @@ static int16_t M_GetFloorTiltHeight( } static int16_t M_GetCeilingTiltHeight( - const SECTOR *sector, const int32_t x, const int32_t z) + const SECTOR *sector, const int32_t x, const int32_t z, + const bool fix_tilts) { int16_t height = sector->ceiling.height; - if (sector->ceiling.tilt == 0) { + if (sector->ceiling.tilt == 0 || (height == NO_HEIGHT && fix_tilts)) { return height; } @@ -294,6 +298,8 @@ void Room_FlipMap(void) room->effect_num = flipped->effect_num; M_AddFlipItems(room); + Output_ObserveRoomFlip(flipped); + Output_ObserveRoomFlip(room); } MovableBlock_HandleFlipMap(RFS_FLIPPED); @@ -461,6 +467,17 @@ int32_t Room_FindByPos(const int32_t x, const int32_t y, const int32_t z) return NO_ROOM_NEG; } +int32_t Room_GetFlippedBaseRoom(const int32_t room_num) +{ + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + if (room->flipped_room == room_num) { + return i; + } + } + return NO_ROOM; +} + BOUNDS_32 Room_GetWorldBounds(void) { BOUNDS_32 bounds = { @@ -544,7 +561,15 @@ HEIGHT_TYPE Room_GetHeightType(void) } int16_t Room_GetHeight( - const SECTOR *sector, const int32_t x, const int32_t y, const int32_t z) + const SECTOR *const sector, const int32_t x, const int32_t y, + const int32_t z) +{ + return Room_GetHeightEx(sector, x, y, z, false); +} + +int16_t Room_GetHeightEx( + const SECTOR *sector, const int32_t x, const int32_t y, const int32_t z, + const bool fix_tilts) { m_HeightType = HT_WALL; @@ -554,7 +579,7 @@ int16_t Room_GetHeight( if (Room_IsAbyssHeight(height)) { height = m_AbyssMaxHeight; } else { - height = M_GetFloorTiltHeight(pit_sector, x, z); + height = M_GetFloorTiltHeight(pit_sector, x, z, fix_tilts); } if (pit_sector->trigger == nullptr) { @@ -580,9 +605,16 @@ int16_t Room_GetHeight( int16_t Room_GetCeiling( const SECTOR *const sector, const int32_t x, const int32_t y, const int32_t z) +{ + return Room_GetCeilingEx(sector, x, y, z, false); +} + +int16_t Room_GetCeilingEx( + const SECTOR *const sector, const int32_t x, const int32_t y, + const int32_t z, const bool fix_tilts) { const SECTOR *const sky_sector = Room_GetSkySector(sector, x, z); - int16_t height = M_GetCeilingTiltHeight(sky_sector, x, z); + int16_t height = M_GetCeilingTiltHeight(sky_sector, x, z, fix_tilts); const SECTOR *const pit_sector = Room_GetPitSector(sector, x, z); if (pit_sector->trigger == nullptr) { diff --git a/src/libtrx/game/savegame.c b/src/libtrx/game/savegame.c deleted file mode 100644 index e6b29488c..000000000 --- a/src/libtrx/game/savegame.c +++ /dev/null @@ -1,22 +0,0 @@ -#include "game/savegame.h" - -#include "log.h" - -static int32_t m_BoundSlot = -1; - -void Savegame_BindSlot(const int32_t slot_num) -{ - m_BoundSlot = slot_num; - LOG_DEBUG("Binding save slot %d", slot_num); -} - -void Savegame_UnbindSlot(void) -{ - LOG_DEBUG("Resetting the save slot"); - m_BoundSlot = -1; -} - -int32_t Savegame_GetBoundSlot(void) -{ - return m_BoundSlot; -} diff --git a/src/libtrx/game/savegame/common.c b/src/libtrx/game/savegame/common.c new file mode 100644 index 000000000..298f6914f --- /dev/null +++ b/src/libtrx/game/savegame/common.c @@ -0,0 +1,641 @@ +#include "benchmark.h" +#include "config.h" +#include "debug.h" +#include "enum_map.h" +#include "game/game.h" +#include "game/game_flow.h" +#include "game/gun/const.h" +#include "game/inventory.h" +#include "game/lara.h" +#include "game/objects.h" +#include "game/objects/traps/movable_block.h" +#include "game/pathing/lot.h" +#include "game/savegame.h" +#include "memory.h" + +#define MAX_STRATEGIES 2 +#define SAVES_DIR "saves" + +static SAVEGAME_VERSION m_InitialVersion = VERSION_LEGACY; +static SAVEGAME_INFO *m_SavegameInfo = nullptr; +static RESUME_INFO *m_ResumeInfo = nullptr; +static STATS_COMMON *m_DefaultStats = nullptr; +static int32_t m_SaveSlots = 0; +static int32_t m_SavedGames = 0; +static int32_t m_SaveCounter = 0; +static int32_t m_MostRecentlyUsedSlot = -1; +static int32_t m_MostRecentlyCreatedSlot = -1; +static int32_t m_BoundSlot = -1; + +static int32_t m_StrategyCount = 0; +static SAVEGAME_STRATEGY m_Strategies[MAX_STRATEGIES]; + +static void M_ClearSlots(void); +static bool M_FillSlot( + SAVEGAME_STRATEGY strategy, int32_t slot_num, const char *path); +static void M_ScanSavedGamesDir(const char *dir_path); +static void M_LoadPreprocess(void); +static void M_LoadPostprocess(void); + +static void M_ClearSlots(void) +{ + if (m_SavegameInfo == nullptr) { + return; + } + + for (int32_t i = 0; i < m_SaveSlots; i++) { + SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i]; + savegame_info->format = SAVEGAME_FORMAT_INVALID; + savegame_info->counter = -1; + savegame_info->level_num = -1; + Memory_FreePointer(&savegame_info->full_path); + Memory_FreePointer(&savegame_info->level_title); + } +} + +static bool M_FillSlot( + const SAVEGAME_STRATEGY strategy, const int32_t slot_num, + const char *const path) +{ + SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num]; + if (strategy.format <= savegame_info->format) { + return true; + } + + bool result = false; + MYFILE *const fp = File_Open(path, FILE_OPEN_READ); + if (fp != nullptr) { + if (strategy.fill_info_func(fp, savegame_info)) { + savegame_info->format = strategy.format; + Memory_FreePointer(&savegame_info->full_path); + savegame_info->full_path = Memory_DupStr(path); + result = true; + } + File_Close(fp); + } + return result; +} + +static void M_ScanSavedGamesDir(const char *const dir_path) +{ + void *const dir_handle = File_OpenDirectory(dir_path); + if (dir_handle == nullptr) { + return; + } + + while (true) { + const char *const file_name = File_ReadDirectory(dir_handle); + if (file_name == nullptr) { + break; + } + if (strcmp(file_name, ".") == 0 || strcmp(file_name, "..") == 0) { + continue; + } + + for (int32_t i = 0; i < m_StrategyCount; i++) { + const SAVEGAME_STRATEGY strategy = m_Strategies[i]; + if (!strategy.allow_load) { + continue; + } + + int32_t slot = -1; + const int32_t parsed = + sscanf(file_name, strategy.get_save_file_pattern_func(), &slot); + if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) { + char *file_path = String_Format("%s/%s", dir_path, file_name); + M_FillSlot(strategy, slot, file_path); + Memory_FreePointer(&file_path); + } + } + } + + File_CloseDirectory(dir_handle); +} + +static void M_LoadPreprocess(void) +{ + Savegame_InitCurrentInfo(); +} + +static void M_LoadPostprocess(void) +{ + // TODO: tidy this; skidoo drivers currently require handle_save_func to be + // called immediately on load within the strategies. + for (int32_t i = 0; i < Item_GetLevelCount(); i++) { + ITEM *const item = Item_Get(i); + const OBJECT *const obj = Object_Get(item->object_id); + + if (obj->save_position && obj->shadow_size) { + int16_t room_num = item->room_num; + const SECTOR *const sector = Room_GetSector( + item->pos.x, item->pos.y, item->pos.z, &room_num); + item->floor = + Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); + } + + if (obj->save_flags != 0) { + item->flags &= 0xFF00; + } +#if TR_VERSION == 1 + if (obj->handle_save_func != nullptr) { + obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD); + } +#endif + } + + MovableBlock_SetupFloor(); + + LARA_INFO *const lara = Lara_GetLaraInfo(); +#if TR_VERSION == 1 + if (Game_GetBonusFlag() != GBF_NONE) { + g_Config.profile.new_game_plus_unlock = true; + } + LOT_ClearLOT(&lara->lot); +#else + if (lara->burn) { + lara->burn = 0; + Lara_CatchFire(); + } +#endif +} + +SAVEGAME_VERSION Savegame_GetInitialVersion(void) +{ + return m_InitialVersion; +} + +void Savegame_SetInitialVersion(const SAVEGAME_VERSION version) +{ + m_InitialVersion = version; +} + +void Savegame_BindSlot(const int32_t slot_num) +{ + m_BoundSlot = slot_num; + m_MostRecentlyUsedSlot = slot_num; + LOG_DEBUG("Binding save slot %d", slot_num); +} + +int32_t Savegame_GetMostRecentlyUsedSlot(void) +{ + return m_MostRecentlyUsedSlot; +} + +void Savegame_UnbindSlot(void) +{ + LOG_DEBUG("Resetting the save slot"); + m_BoundSlot = -1; +} + +int32_t Savegame_GetBoundSlot(void) +{ + return m_BoundSlot; +} + +int32_t Savegame_GetLevelNumber(const int32_t slot_num) +{ + return m_SavegameInfo[slot_num].level_num; +} + +bool Savegame_IsSlotFree(const int32_t slot_num) +{ + return m_SavegameInfo[slot_num].level_num == -1; +} + +int32_t Savegame_GetCounter(void) +{ + return m_SaveCounter; +} + +int32_t Savegame_GetTotalCount(void) +{ + return m_SavedGames; +} + +int32_t Savegame_GetMostRecentlyCreatedSlot(void) +{ + return m_MostRecentlyCreatedSlot; +} + +bool Savegame_RestartAvailable(const int32_t slot_num) +{ +#if TR_VERSION == 1 + if (slot_num == -1) { + return true; + } + + const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num]; + return savegame_info->features.restart; +#else + return false; +#endif +} + +void Savegame_RegisterStrategy(const SAVEGAME_STRATEGY strategy) +{ + ASSERT(m_StrategyCount < MAX_STRATEGIES); + m_Strategies[m_StrategyCount] = strategy; + m_StrategyCount++; +} + +void Savegame_Init(void) +{ + m_ResumeInfo = Memory_Alloc( + sizeof(RESUME_INFO) + * (GF_GetLevelTable(GFLT_MAIN)->count + + GF_GetLevelTable(GFLT_DEMOS)->count)); + + m_SaveSlots = Savegame_GetSlotCount(); + m_SavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots); + + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS); + for (int32_t i = 0; i < level_table->count; i++) { + RESUME_INFO *const resume_info = + Savegame_GetCurrentInfo(&level_table->levels[i]); + resume_info->flags.available = 1; + resume_info->flags.has_pistols = 1; + resume_info->pistol_ammo = 1000; + resume_info->gun_status = LGS_ARMLESS; + resume_info->equipped_gun_type = LGT_PISTOLS; +#if TR_VERSION == 1 + resume_info->holsters_gun_type = LGT_PISTOLS; + resume_info->back_gun_type = LGT_UNARMED; + resume_info->lara_hitpoints = LARA_MAX_HITPOINTS; +#endif + } +} + +bool Savegame_IsInitialised(void) +{ + return m_SavegameInfo != nullptr; +} + +void Savegame_Shutdown(void) +{ + M_ClearSlots(); + Memory_FreePointer(&m_ResumeInfo); + Memory_FreePointer(&m_SavegameInfo); + Memory_FreePointer(&m_DefaultStats); +} + +RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level) +{ + ASSERT(m_ResumeInfo != nullptr); + ASSERT(level != nullptr); + if (GF_GetLevelTableType(level->type) == GFLT_MAIN) { + return &m_ResumeInfo[level->num]; + } else if (level->type == GFL_DEMO) { + return &m_ResumeInfo[GF_GetLevelTable(GFLT_MAIN)->count]; + } + LOG_WARNING( + "Warning: unable to get resume info for level %d (type=%s)", level->num, + ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type)); + return nullptr; +} + +void Savegame_SetCurrentInfo(const int32_t current_slot, const int32_t src_slot) +{ + m_ResumeInfo[current_slot] = m_ResumeInfo[src_slot]; +} + +const SAVEGAME_INFO *Savegame_GetSavegameInfo(const int32_t slot_num) +{ + return &m_SavegameInfo[slot_num]; +} + +void Savegame_InitCurrentInfo(void) +{ + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); + for (int32_t i = 0; i < level_table->count; i++) { + const GF_LEVEL *const level = &level_table->levels[i]; + Savegame_ResetCurrentInfo(level); + Savegame_ApplyLogicToCurrentInfo(level); + Savegame_GetCurrentInfo(level)->flags.available = 0; + } + + if (GF_GetGymLevel() != nullptr) { + Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = 1; + } + if (GF_GetFirstLevel() != nullptr) { + Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = 1; + } +} + +void Savegame_ResetCurrentInfo(const GF_LEVEL *const level) +{ + LOG_INFO("Resetting resume info for level #%d", level->num); + RESUME_INFO *const current = Savegame_GetCurrentInfo(level); + memset(current, 0, sizeof(RESUME_INFO)); +} + +void Savegame_CarryCurrentInfoToNextLevel( + const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level) +{ + LOG_INFO( + "Copying resume info from level #%d to level #%d", src_level->num, + dst_level->num); + RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level); + RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level); + memcpy(dst_resume, src_resume, sizeof(RESUME_INFO)); +} + +void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level) +{ + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + LARA_INFO *const lara = Lara_GetLaraInfo(); + +#if TR_VERSION == 1 + resume->lara_hitpoints = Lara_GetItem()->hit_points; +#endif + resume->flags.available = 1; + resume->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM); + resume->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM); + + resume->pistol_ammo = 1000; + if (Inv_RequestItem(O_PISTOL_ITEM)) { + resume->flags.has_pistols = 1; + } else { + resume->flags.has_pistols = 0; + } + + if (Inv_RequestItem(O_SHOTGUN_ITEM)) { + resume->flags.has_shotgun = 1; + resume->shotgun_ammo = lara->shotgun_ammo.ammo; + } else { + resume->flags.has_shotgun = 0; + resume->shotgun_ammo = + Inv_RequestItem(O_SHOTGUN_AMMO_ITEM) * SHOTGUN_AMMO_QTY; + } + + if (Inv_RequestItem(O_MAGNUM_ITEM)) { + resume->flags.has_magnums = 1; + resume->magnum_ammo = lara->magnum_ammo.ammo; + } else { + resume->flags.has_magnums = 0; + resume->magnum_ammo = + Inv_RequestItem(O_MAGNUM_AMMO_ITEM) * MAGNUM_AMMO_QTY; + } + + if (Inv_RequestItem(O_UZI_ITEM)) { + resume->flags.has_uzis = 1; + resume->uzi_ammo = lara->uzi_ammo.ammo; + } else { + resume->flags.has_uzis = 0; + resume->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * UZI_AMMO_QTY; + } + +#if TR_VERSION == 1 + resume->num_scions = Inv_RequestItem(O_SCION_ITEM_1); + + resume->equipped_gun_type = lara->gun_type; + resume->holsters_gun_type = lara->holsters_gun_type; + resume->back_gun_type = lara->back_gun_type; + if (lara->gun_status == LGS_READY) { + resume->gun_status = LGS_READY; + } else { + resume->gun_status = LGS_ARMLESS; + } +#elif TR_VERSION == 2 + if (Inv_RequestItem(O_M16_ITEM)) { + resume->flags.has_m16 = 1; + resume->m16_ammo = lara->m16_ammo.ammo; + } else { + resume->flags.has_m16 = 0; + resume->m16_ammo = Inv_RequestItem(O_M16_AMMO_ITEM) * M16_AMMO_QTY; + } + + if (Inv_RequestItem(O_HARPOON_ITEM)) { + resume->flags.has_harpoon = 1; + resume->harpoon_ammo = lara->harpoon_ammo.ammo; + } else { + resume->flags.has_harpoon = 0; + resume->harpoon_ammo = + Inv_RequestItem(O_HARPOON_AMMO_ITEM) * HARPOON_AMMO_QTY; + } + + if (Inv_RequestItem(O_GRENADE_ITEM)) { + resume->flags.has_grenade = 1; + resume->grenade_ammo = lara->grenade_ammo.ammo; + } else { + resume->flags.has_grenade = 0; + resume->grenade_ammo = + Inv_RequestItem(O_GRENADE_AMMO_ITEM) * GRENADE_AMMO_QTY; + } + + resume->flares = Inv_RequestItem(O_FLARE_ITEM); + if (lara->gun_type == LGT_FLARE) { + resume->equipped_gun_type = lara->last_gun_type; + } else { + resume->equipped_gun_type = lara->gun_type; + } + resume->gun_status = LGS_ARMLESS; +#endif +} + +void Savegame_ProcessItemsBeforeSave(void) +{ + for (int32_t i = 0; i < Item_GetLevelCount(); i++) { + ITEM *const item = Item_Get(i); + const OBJECT *const obj = Object_Get(item->object_id); + if (obj->handle_save_func != nullptr) { + obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE); + } + } +} + +void Savegame_ProcessItemsBeforeLoad(void) +{ + for (int32_t i = 0; i < Item_GetLevelCount(); i++) { + ITEM *const item = Item_Get(i); + const OBJECT *const obj = Object_Get(item->object_id); + if (obj->handle_save_func != nullptr) { + obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD); + } + } +} + +void Savegame_SetDefaultStats( + const GF_LEVEL *const level, const STATS_COMMON stats) +{ + if (m_DefaultStats == nullptr) { + m_DefaultStats = Memory_Alloc( + sizeof(STATS_COMMON) * GF_GetLevelTable(GFLT_MAIN)->count); + } + m_DefaultStats[level->num] = stats; +} + +STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *const level) +{ + if (m_DefaultStats == nullptr + || (level->type != GFL_NORMAL && level->type != GFL_BONUS)) { + return (STATS_COMMON) {}; + } + return m_DefaultStats[level->num]; +} + +void Savegame_ScanSavedGames(void) +{ + BENCHMARK benchmark = Benchmark_Start(); + M_ClearSlots(); + + m_SaveCounter = 0; + m_SavedGames = 0; + m_MostRecentlyCreatedSlot = -1; + + M_ScanSavedGamesDir(SAVES_DIR); + M_ScanSavedGamesDir("."); + + for (int32_t i = 0; i < m_SaveSlots; i++) { + SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i]; + if (savegame_info->level_title != nullptr) { + if (savegame_info->counter > m_SaveCounter) { + m_SaveCounter = savegame_info->counter; + m_MostRecentlyCreatedSlot = i; + } + m_SavedGames++; + } + } + + Benchmark_End(&benchmark, nullptr); +} + +bool Savegame_Save(const int32_t slot_idx) +{ + bool result = false; + Savegame_BindSlot(slot_idx); + + File_CreateDirectory(SAVES_DIR); + + const GF_LEVEL *const current_level = Game_GetCurrentLevel(); + const char *const level_title = current_level->title; + + Savegame_PersistGameToCurrentInfo(current_level); + +#if TR_VERSION == 1 + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); + for (int32_t i = 0; i < level_table->count; i++) { + const GF_LEVEL *const level = &level_table->levels[i]; + if (level->type == GFL_CURRENT) { + Savegame_SetCurrentInfo(i, current_level->num); + } + } +#endif + + SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx]; + const bool was_slot_empty = savegame_info->full_path == nullptr; + + m_SaveCounter++; + for (int32_t i = 0; i < m_StrategyCount; i++) { + const SAVEGAME_STRATEGY strategy = m_Strategies[i]; + if (!strategy.allow_save || strategy.save_to_file_func == nullptr) { + continue; + } + + char *file_name = + String_Format(strategy.get_save_file_pattern_func(), slot_idx); + char *full_path = String_Format("%s/%s", SAVES_DIR, file_name); + MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE); + if (fp != nullptr) { + strategy.save_to_file_func(fp, savegame_info); + savegame_info->format = strategy.format; + Memory_FreePointer(&savegame_info->full_path); + savegame_info->full_path = Memory_DupStr(File_GetPath(fp)); + savegame_info->counter = m_SaveCounter; + savegame_info->level_num = current_level->num; + savegame_info->level_title = + level_title != nullptr ? Memory_DupStr(level_title) : nullptr; + File_Close(fp); + result = true; + } + + Memory_FreePointer(&file_name); + Memory_FreePointer(&full_path); + } + + if (result) { + m_MostRecentlyCreatedSlot = slot_idx; + if (was_slot_empty) { + m_SavedGames++; + } + Savegame_HighlightNewestSlot(); + } else { + m_SaveCounter--; + } + + return result; +} + +bool Savegame_Load(const int32_t slot_idx) +{ + const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx]; + ASSERT(savegame_info->format != 0); + + M_LoadPreprocess(); + + bool result = false; + for (int32_t i = 0; i < m_StrategyCount; i++) { + const SAVEGAME_STRATEGY strategy = m_Strategies[i]; + if (strategy.format != savegame_info->format) { + continue; + } + + MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ); + if (fp != nullptr) { + result = strategy.load_from_file_func(fp); + File_Close(fp); + } + break; + } + + M_LoadPostprocess(); + m_InitialVersion = m_SavegameInfo[slot_idx].initial_version; + return result; +} + +bool Savegame_UpdateDeathCounters( + const int32_t slot_num, const int32_t death_count) +{ + ASSERT(slot_num >= 0); + const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num]; + ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID); + + bool ret = false; + for (int32_t i = 0; i < m_StrategyCount; i++) { + const SAVEGAME_STRATEGY strategy = m_Strategies[i]; + if (savegame_info->format == strategy.format + && strategy.update_death_counters_func != nullptr) { + MYFILE *const fp = + File_Open(savegame_info->full_path, FILE_OPEN_READ_WRITE); + if (fp != nullptr) { + ret = strategy.update_death_counters_func(fp, death_count); + File_Close(fp); + } + break; + } + } + return ret; +} + +bool Savegame_LoadOnlyResumeInfo(int32_t slot_num) +{ + const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num]; + ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID); + + bool ret = false; + for (int32_t i = 0; i < m_StrategyCount; i++) { + const SAVEGAME_STRATEGY strategy = m_Strategies[i]; + if (savegame_info->format == strategy.format + && strategy.load_only_resume_info_func != nullptr) { + MYFILE *const fp = + File_Open(savegame_info->full_path, FILE_OPEN_READ); + if (fp != nullptr) { + ret = strategy.load_only_resume_info_func(fp); + File_Close(fp); + } + break; + } + } + + Savegame_SetInitialVersion(m_SavegameInfo[slot_num].initial_version); + return ret; +} diff --git a/src/tr2/game/scaler.c b/src/libtrx/game/scaler.c similarity index 66% rename from src/tr2/game/scaler.c rename to src/libtrx/game/scaler.c index d8595f125..38913d0a5 100644 --- a/src/tr2/game/scaler.c +++ b/src/libtrx/game/scaler.c @@ -1,20 +1,21 @@ #include "game/scaler.h" -#include "global/vars.h" - -#include -#include -#include +#include "config.h" +#include "game/viewport.h" +#include "log.h" +#include "utils.h" static int32_t M_DoCalc( int32_t unit, int32_t base_width, int32_t base_height, double factor) { + const int32_t win_width = Viewport_GetWidth(); + const int32_t win_height = Viewport_GetHeight(); const int32_t sign = unit < 0 ? -1 : 1; - const int32_t scale_x = g_PhdWinWidth > base_width - ? ((double)g_PhdWinWidth * ABS(unit) * factor) / base_width + const int32_t scale_x = win_width > base_width + ? ((double)win_width * ABS(unit) * factor) / MAX(1, base_width) : ABS(unit) * factor; - const int32_t scale_y = g_PhdWinHeight > base_height - ? ((double)g_PhdWinHeight * ABS(unit) * factor) / base_height + const int32_t scale_y = win_height > base_height + ? ((double)win_height * ABS(unit) * factor) / MAX(1, base_height) : ABS(unit) * factor; return MIN(scale_x, scale_y) * sign; } diff --git a/src/libtrx/game/shell/common.c b/src/libtrx/game/shell/common.c index 2e180264e..391ae69f9 100644 --- a/src/libtrx/game/shell/common.c +++ b/src/libtrx/game/shell/common.c @@ -130,31 +130,42 @@ void Shell_ExitSystemFmt(const char *fmt, ...) Memory_FreePointer(&message); } -int32_t Shell_GetCurrentDisplayWidth(void) +bool Shell_IsFullscreen(void) { - SDL_DisplayMode dm; - SDL_GetCurrentDisplayMode(0, &dm); - return dm.w; -} - -int32_t Shell_GetCurrentDisplayHeight(void) -{ - SDL_DisplayMode dm; - SDL_GetCurrentDisplayMode(0, &dm); - return dm.h; -} - -void Shell_GetWindowSize(int32_t *const out_width, int32_t *const out_height) -{ - ASSERT(out_width != nullptr); - ASSERT(out_height != nullptr); SDL_Window *const window = Shell_GetWindow(); - if (window == nullptr) { - *out_width = -1; - *out_height = -1; - } else { - SDL_GetWindowSize(window, out_width, out_height); + ASSERT(window != nullptr); + const Uint32 flags = SDL_GetWindowFlags(window); + return (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; +} + +SHELL_SIZE Shell_GetWindowSize(void) +{ + SDL_Window *const window = Shell_GetWindow(); + SHELL_SIZE result = { .w = -1, .h = -1 }; + if (window != nullptr) { + SDL_GetWindowSize(window, &result.w, &result.h); } + return result; +} + +SHELL_SIZE Shell_GetCurrentSize(void) +{ + return Shell_IsFullscreen() ? Shell_GetCurrentDisplaySize() + : Shell_GetWindowSize(); +} + +SHELL_SIZE Shell_GetCurrentDisplaySize(void) +{ + int32_t display_idx = 0; + SDL_Window *const window = Shell_GetWindow(); + if (window != nullptr) { + display_idx = SDL_GetWindowDisplayIndex(window); + } + SDL_DisplayMode dm; + if (SDL_GetCurrentDisplayMode(display_idx, &dm) == 0) { + return (SHELL_SIZE) { .w = dm.w, .h = dm.h }; + } + return (SHELL_SIZE) { .w = -1, .h = -1 }; } void Shell_ScheduleExit(void) diff --git a/src/libtrx/game/text.c b/src/libtrx/game/text.c index 28bfdedec..5d280ebcc 100644 --- a/src/libtrx/game/text.c +++ b/src/libtrx/game/text.c @@ -11,7 +11,14 @@ typedef struct { GLYPH_INFO *glyph; UT_hash_handle hh; -} M_HASH_ENTRY; +} M_GLYPH_MAP_ENTRY; + +typedef struct { + char *text; + const GLYPH_INFO **glyphs; + size_t glyph_count; + UT_hash_handle hh; +} M_TEXT_MAP_ENTRY; static TEXTSTRING m_TextStrings[TEXT_MAX_STRINGS] = {}; @@ -28,7 +35,8 @@ static GLYPH_INFO m_Glyphs[] = { static size_t m_GlyphLookupKeyCap = 0; static char *m_GlyphLookupKey = nullptr; -static M_HASH_ENTRY *m_GlyphMap = nullptr; +static M_GLYPH_MAP_ENTRY *m_GlyphMap = nullptr; +static M_TEXT_MAP_ENTRY *m_TextMap = nullptr; static size_t M_GetGlyphSize(const char *ptr); @@ -66,7 +74,8 @@ void Text_Init(void) // table for faster text-to-glyph resolution. for (GLYPH_INFO *glyph_ptr = m_Glyphs; glyph_ptr->text != nullptr; glyph_ptr++) { - M_HASH_ENTRY *const hash_entry = Memory_Alloc(sizeof(M_HASH_ENTRY)); + M_GLYPH_MAP_ENTRY *const hash_entry = + Memory_Alloc(sizeof(M_GLYPH_MAP_ENTRY)); hash_entry->glyph = glyph_ptr; HASH_ADD_KEYPTR( hh, m_GlyphMap, glyph_ptr->text, strlen(glyph_ptr->text), @@ -82,14 +91,29 @@ void Text_Shutdown(void) for (int32_t i = 0; i < TEXT_MAX_STRINGS; i++) { TEXTSTRING *const text = &m_TextStrings[i]; Memory_FreePointer(&text->content); + text->content_cap = 0; Memory_FreePointer(&text->glyphs); + text->glyphs_cap = 0; } - M_HASH_ENTRY *current, *tmp; - HASH_ITER(hh, m_GlyphMap, current, tmp) { - HASH_DEL(m_GlyphMap, current); - Memory_Free(current); + M_GLYPH_MAP_ENTRY *current, *tmp; + HASH_ITER(hh, m_GlyphMap, current, tmp) + { + HASH_DEL(m_GlyphMap, current); + Memory_Free(current); + } + } + + { + M_TEXT_MAP_ENTRY *current, *tmp; + HASH_ITER(hh, m_TextMap, current, tmp) + { + Memory_FreePointer(¤t->text); + Memory_FreePointer(¤t->glyphs); + HASH_DEL(m_TextMap, current); + Memory_FreePointer(¤t); + } } Memory_FreePointer(&m_GlyphLookupKey); @@ -134,8 +158,12 @@ TEXTSTRING *Text_Create(int16_t x, int16_t y, const char *const content) } TEXTSTRING *text = &m_TextStrings[free_idx]; - text->content = nullptr; - text->glyphs = nullptr; + if (text->content != nullptr) { + text->content[0] = '\0'; + } + if (text->glyphs != nullptr) { + text->glyphs[0] = nullptr; + } text->scale.h = TEXT_BASE_SCALE; text->scale.v = TEXT_BASE_SCALE; text->pos.x = x; @@ -164,24 +192,18 @@ void Text_Remove(TEXTSTRING *const text) } if (text->flags.active) { text->flags.active = 0; - Memory_FreePointer(&text->content); - Memory_FreePointer(&text->glyphs); + if (text->content != nullptr) { + text->content[0] = '\0'; + } + if (text->glyphs != nullptr) { + text->glyphs[0] = nullptr; + } } } -void Text_ChangeText(TEXTSTRING *const text, const char *const content) +static const GLYPH_INFO **M_Decompose( + const char *const content, size_t *out_glyph_count) { - if (text == nullptr) { - return; - } - - ASSERT(content != nullptr); - Memory_FreePointer(&text->content); - Memory_FreePointer(&text->glyphs); - if (!text->flags.active) { - return; - } - // Count number of characters size_t glyph_count = 0; const char *content_ptr = content; @@ -191,12 +213,11 @@ void Text_ChangeText(TEXTSTRING *const text, const char *const content) glyph_count++; } - text->content = Memory_DupStr(content); - text->glyphs = Memory_Alloc((glyph_count + 1) * sizeof(GLYPH_INFO *)); - // Assign glyphs using hash table + const GLYPH_INFO **glyphs = + Memory_Alloc((glyph_count + 1) * sizeof(GLYPH_INFO *)); content_ptr = content; - const GLYPH_INFO **glyph_ptr = text->glyphs; + const GLYPH_INFO **glyph_ptr = glyphs; while (*content_ptr != '\0') { const size_t glyph_size = M_GetGlyphSize(content_ptr); if (m_GlyphLookupKeyCap <= glyph_size) { @@ -207,7 +228,7 @@ void Text_ChangeText(TEXTSTRING *const text, const char *const content) strncpy(m_GlyphLookupKey, content_ptr, glyph_size); m_GlyphLookupKey[glyph_size] = '\0'; - M_HASH_ENTRY *entry; + M_GLYPH_MAP_ENTRY *entry; HASH_FIND_STR(m_GlyphMap, m_GlyphLookupKey, entry); if (entry != nullptr) { @@ -219,8 +240,67 @@ void Text_ChangeText(TEXTSTRING *const text, const char *const content) content_ptr += glyph_size; } + if (out_glyph_count != nullptr) { + *out_glyph_count = glyph_count; + } + // guard *glyph_ptr++ = nullptr; + return glyphs; +} + +static const GLYPH_INFO **M_DecomposeWithCache( + const char *const content, size_t *out_glyph_count) +{ + M_TEXT_MAP_ENTRY *entry; + HASH_FIND_STR(m_TextMap, content, entry); + if (entry == nullptr) { + entry = Memory_Alloc(sizeof(M_TEXT_MAP_ENTRY)); + entry->text = Memory_DupStr(content); + entry->glyphs = M_Decompose(content, &entry->glyph_count); + HASH_ADD_STR(m_TextMap, text, entry); + } + if (out_glyph_count != nullptr) { + *out_glyph_count = entry->glyph_count; + } + return entry->glyphs; +} + +void Text_ChangeText(TEXTSTRING *const text, const char *const content) +{ + if (text == nullptr) { + return; + } + + ASSERT(content != nullptr); + if (text->content != nullptr) { + text->content[0] = '\0'; + } + if (text->glyphs != nullptr) { + text->glyphs[0] = nullptr; + } + if (!text->flags.active) { + return; + } + + const size_t content_size = strlen(content) + 1; + if (content_size >= text->content_cap) { + text->content_cap = content_size; + text->content = Memory_Realloc(text->content, content_size); + } + strcpy(text->content, content); + + size_t glyph_count; + const GLYPH_INFO **glyphs = + M_DecomposeWithCache(text->content, &glyph_count); + const size_t glyphs_size = (glyph_count + 1) * sizeof(GLYPH_INFO *); + if (glyphs_size >= text->glyphs_cap) { + text->glyphs_cap = glyphs_size; + text->glyphs = Memory_Realloc(text->glyphs, glyphs_size); + } + text->glyphs[0] = nullptr; + memcpy(text->glyphs, glyphs, glyphs_size); + // Memory_FreePointer(&glyphs); } void Text_SetPos(TEXTSTRING *const text, int16_t x, int16_t y) @@ -355,11 +435,7 @@ void Text_SetMultiline(TEXTSTRING *const text, const bool enable) int32_t Text_GetWidth(const TEXTSTRING *const text) { - if (text == nullptr) { - return 0; - } - - if (text->glyphs == nullptr) { + if (text == nullptr || text->glyphs == nullptr) { return 0; } @@ -395,5 +471,5 @@ int32_t Text_GetHeight(const TEXTSTRING *const text) height += TEXT_HEIGHT_FIXED; } } - return height * text->scale.v / TEXT_BASE_SCALE; + return height * text->scale.v / (float)TEXT_BASE_SCALE; } diff --git a/src/libtrx/game/ui/common.c b/src/libtrx/game/ui/common.c index 84b0175d9..2ffa2c32b 100644 --- a/src/libtrx/game/ui/common.c +++ b/src/libtrx/game/ui/common.c @@ -1,17 +1,174 @@ #include "game/ui/common.h" #include "config.h" +#include "debug.h" #include "game/console/common.h" #include "game/game_string.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/events.h" +#include "memory.h" + +#include +#include + +static struct { + MEMORY_ARENA_ALLOCATOR alloc; + + int32_t node_count; + int32_t node_capacity; + UI_NODE *nodes; + + UI_NODE *root; // The top-level container + UI_NODE *current; // The current container into which we attach nodes +} m_Priv = { + .alloc = { + .default_chunk_size = 1024 * 4, + }, +}; + +static UI_INPUT M_TranslateInput(uint32_t system_keycode); +static void M_MeasureNode(UI_NODE *node); +static void M_LayoutNode(UI_NODE *node, float x, float y, float w, float h); +static void M_DrawNode(const UI_NODE *node); + +static UI_INPUT M_TranslateInput(const uint32_t system_keycode) +{ + // clang-format off + switch (system_keycode) { + case SDLK_UP: return UI_KEY_UP; + case SDLK_DOWN: return UI_KEY_DOWN; + case SDLK_LEFT: return UI_KEY_LEFT; + case SDLK_RIGHT: return UI_KEY_RIGHT; + case SDLK_HOME: return UI_KEY_HOME; + case SDLK_END: return UI_KEY_END; + case SDLK_BACKSPACE: return UI_KEY_BACK; + case SDLK_RETURN: return UI_KEY_RETURN; + case SDLK_ESCAPE: return UI_KEY_ESCAPE; + } + // clang-format on + return -1; +} + +// Depth-first measure pass +static void M_MeasureNode(UI_NODE *const node) +{ + if (node == nullptr || node->ops == nullptr + || node->ops->measure == nullptr) { + return; + } + + // Recurse to children + UI_NODE *child = node->first_child; + while (child != nullptr) { + M_MeasureNode(child); + child = child->next_sibling; + } + + node->ops->measure(node); +} + +// Depth-first layout pass +static void M_LayoutNode( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + if (node == nullptr || node->ops == nullptr + || node->ops->layout == nullptr) { + return; + } + + node->ops->layout(node, x, y, w, h); + // Recursing to children is a responsibility of the layout function. +} + +// Depth-first draw pass +static void M_DrawNode(const UI_NODE *const node) +{ + if (node == nullptr || node->ops == nullptr || node->ops->draw == nullptr) { + return; + } + + node->ops->draw(node); + // Recursing to children is a responsibility of the draw function. +} + +// Allocate a new node +UI_NODE *UI_AllocNode( + const UI_WIDGET_OPS *const ops, const size_t additional_size) +{ + const size_t size = sizeof(UI_NODE) + additional_size; + UI_NODE *const node = Memory_ArenaAlloc(&m_Priv.alloc, size); + memset(node, 0, size); + node->ops = ops; + node->data = (char *)node + sizeof(UI_NODE); + m_Priv.node_count++; + return node; +} + +// Attach child to parent's child list +void UI_AddChild(UI_NODE *const child) +{ + // Special case - the root widget + if (m_Priv.root == nullptr) { + m_Priv.root = child; + return; + } + + UI_NODE *const parent = m_Priv.current; + if (parent == nullptr || child == nullptr) { + return; + } + child->parent = parent; + if (parent->first_child == nullptr) { + parent->first_child = child; + } else { + parent->last_child->next_sibling = child; + } + parent->last_child = child; +} + +void UI_PushCurrent(UI_NODE *const child) +{ + m_Priv.current = child; +} + +void UI_PopCurrent(void) +{ + ASSERT(m_Priv.current != nullptr); + m_Priv.current = m_Priv.current->parent; + if (m_Priv.current == nullptr) { + m_Priv.root = nullptr; + } +} + +// Scene management +void UI_BeginScene(void) +{ + Memory_ArenaReset(&m_Priv.alloc); + m_Priv.node_count = 0; + + // Make a root node. + UI_BeginAnchor(0.5f, 0.5f); +} + +void UI_EndScene(void) +{ + M_MeasureNode(m_Priv.root); + M_LayoutNode(m_Priv.root, 0, 0, UI_GetCanvasWidth(), UI_GetCanvasHeight()); + M_DrawNode(m_Priv.root); + UI_EndAnchor(); + ASSERT(m_Priv.root == nullptr); +} void UI_Init(void) { - UI_Events_Init(); + UI_InitEvents(); } void UI_Shutdown(void) { - UI_Events_Shutdown(); + Memory_ArenaFree(&m_Priv.alloc); + UI_ShutdownEvents(); } void UI_ToggleState(bool *const config_setting) @@ -21,42 +178,26 @@ void UI_ToggleState(bool *const config_setting) Console_Log(*config_setting ? GS(OSD_UI_ON) : GS(OSD_UI_OFF)); } -void UI_HandleLayoutChange(void) -{ - const EVENT event = { - .name = "layout_change", - .sender = nullptr, - .data = nullptr, - }; - UI_Events_Fire(&event); -} - void UI_HandleKeyDown(const uint32_t key) { - const EVENT event = { + UI_FireEvent((EVENT) { .name = "key_down", .sender = nullptr, - .data = (void *)UI_TranslateInput(key), - }; - UI_Events_Fire(&event); + .data = (void *)M_TranslateInput(key), + }); } void UI_HandleKeyUp(const uint32_t key) { - const EVENT event = { + UI_FireEvent((EVENT) { .name = "key_up", .sender = nullptr, - .data = (void *)UI_TranslateInput(key), - }; - UI_Events_Fire(&event); + .data = (void *)M_TranslateInput(key), + }); } void UI_HandleTextEdit(const char *const text) { - const EVENT event = { - .name = "text_edit", - .sender = nullptr, - .data = (void *)text, - }; - UI_Events_Fire(&event); + UI_FireEvent((EVENT) { + .name = "text_edit", .sender = nullptr, .data = (void *)text }); } diff --git a/src/libtrx/game/ui/dialogs/base_passport.c b/src/libtrx/game/ui/dialogs/base_passport.c new file mode 100644 index 000000000..082d7b62d --- /dev/null +++ b/src/libtrx/game/ui/dialogs/base_passport.c @@ -0,0 +1,59 @@ +#include "game/ui/dialogs/base_passport.h" + +#include "game/inventory.h" +#include "game/scaler.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/requester.h" +#include "game/ui/elements/resize.h" +#include "game/viewport.h" + +static int32_t M_GetVisibleRows(void); + +static int32_t M_GetVisibleRows(void) +{ + if (TR_VERSION == 2) { + return 10; + } else { + const int32_t res_h = + Scaler_CalcInverse(Viewport_GetHeight(), SCALER_TARGET_TEXT); + if (res_h <= 240) { + return 5; + } else if (res_h <= 384) { + return 7; + } else if (res_h <= 480) { + return 10; + } else { + return 12; + } + } +} + +void UI_BasePassportDialog_Init( + UI_REQUESTER_STATE *const req, const size_t max_rows) +{ + UI_Requester_Init(req, M_GetVisibleRows(), max_rows, true); + req->row_pad = 4.0f; + req->row_spacing = TR_VERSION == 1 ? 2.0f : 3.0f; + req->show_arrows = TR_VERSION == 1; + req->reserve_space = true; +} + +void UI_BasePassportDialog_Control(UI_REQUESTER_STATE *const req) +{ + UI_Requester_SetVisibleRows(req, M_GetVisibleRows()); +} + +void UI_BeginBasePassportDialog(void) +{ + const float modal_y = TR_VERSION == 1 + ? (g_Inv_Mode == INV_TITLE_MODE ? 0.72f : 0.55f) + : (g_Inv_Mode == INV_TITLE_MODE ? 0.8f : 0.65f); + UI_BeginModal(0.5f, modal_y); + UI_BeginResize(300.0f, -1.0f); +} + +void UI_EndBasePassportDialog(void) +{ + UI_EndResize(); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/controls.c b/src/libtrx/game/ui/dialogs/controls.c new file mode 100644 index 000000000..eea608c4a --- /dev/null +++ b/src/libtrx/game/ui/dialogs/controls.c @@ -0,0 +1,88 @@ +#include "game/ui/dialogs/controls.h" + +#include "config.h" +#include "game/game_string.h" +#include "game/input.h" +#include "game/ui/dialogs/controls_backend.h" +#include "game/ui/dialogs/controls_editor.h" +#include "game/ui/elements/requester.h" + +typedef enum { + M_PHASE_BACKEND, + M_PHASE_EDITOR, +} M_PHASE; + +void UI_Controls_Init(UI_CONTROLS_STATE *const s) +{ + s->events = EventManager_Create(); + s->phase = M_PHASE_BACKEND; + s->backend = INPUT_BACKEND_KEYBOARD; + s->active_layout = g_Config.input.keyboard_layout; + UI_ControlsBackend_Init(&s->backend_state); + UI_ControlsEditor_Init(&s->editor_state, s->events); +} + +void UI_Controls_Free(UI_CONTROLS_STATE *const s) +{ + UI_ControlsEditor_Free(&s->editor_state); + UI_ControlsBackend_Free(&s->backend_state); + EventManager_Free(s->events); + s->events = nullptr; +} + +bool UI_Controls_Control(UI_CONTROLS_STATE *const s) +{ + switch (s->phase) { + case M_PHASE_BACKEND: { + const int32_t choice = UI_ControlsBackend_Control(&s->backend_state); + switch (choice) { + case UI_REQUESTER_NO_CHOICE: + return false; + case UI_REQUESTER_CANCEL: + return true; + case INPUT_BACKEND_KEYBOARD: + s->backend = choice; + s->phase = M_PHASE_EDITOR; + UI_ControlsEditor_Reinit( + &s->editor_state, choice, g_Config.input.keyboard_layout); + break; + case INPUT_BACKEND_CONTROLLER: + s->backend = choice; + s->phase = M_PHASE_EDITOR; + UI_ControlsEditor_Reinit( + &s->editor_state, choice, g_Config.input.controller_layout); + break; + } + break; + } + + case M_PHASE_EDITOR: { + const UI_CONTROLS_CHOICE choice = + UI_ControlsEditor_Control(&s->editor_state); + switch (choice) { + case UI_CONTROLS_CHOICE_NOOP: + break; + case UI_CONTROLS_CHOICE_GO_BACK: + s->phase = M_PHASE_BACKEND; + break; + case UI_CONTROLS_CHOICE_EXIT: + return true; + } + break; + } + } + + return false; +} + +void UI_Controls(UI_CONTROLS_STATE *const s) +{ + switch (s->phase) { + case M_PHASE_BACKEND: + UI_ControlsBackend(&s->backend_state); + break; + case M_PHASE_EDITOR: + UI_ControlsEditor(&s->editor_state); + break; + } +} diff --git a/src/libtrx/game/ui/dialogs/controls_backend.c b/src/libtrx/game/ui/dialogs/controls_backend.c new file mode 100644 index 000000000..58b6f68a9 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/controls_backend.c @@ -0,0 +1,57 @@ +#include "game/ui/dialogs/controls_backend.h" + +#include "game/game_string.h" +#include "game/input.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/requester.h" + +static const GAME_STRING_ID m_Options[] = { + GS_ID(CONTROLS_BACKEND_KEYBOARD), + GS_ID(CONTROLS_BACKEND_CONTROLLER), + nullptr, +}; + +void UI_ControlsBackend_Init(UI_CONTROLS_BACKEND_STATE *const s) +{ + int32_t count = 0; + for (count = 0; m_Options[count] != nullptr; count++) { } + UI_Requester_Init(&s->req, count, count, true); +} + +void UI_ControlsBackend_Free(UI_CONTROLS_BACKEND_STATE *const s) +{ + UI_Requester_Free(&s->req); +} + +int32_t UI_ControlsBackend_Control(UI_CONTROLS_BACKEND_STATE *const s) +{ + const int32_t choice = UI_Requester_Control(&s->req); + switch (choice) { + case 0: + return INPUT_BACKEND_KEYBOARD; + case 1: + return INPUT_BACKEND_CONTROLLER; + default: + return choice; + } +} + +void UI_ControlsBackend(UI_CONTROLS_BACKEND_STATE *const s) +{ + UI_BeginModal(0.5f, 2.0f / 3.0f); + UI_BeginRequester(&s->req, GS(CONTROLS_CUSTOMIZE)); + + for (int32_t i = UI_Requester_GetFirstRow(&s->req); + i < UI_Requester_GetLastRow(&s->req); i++) { + UI_BeginRequesterRow(&s->req, i); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(GameString_Get(m_Options[i])); + UI_EndAnchor(); + UI_EndRequesterRow(&s->req, i); + } + + UI_EndRequester(&s->req); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/controls_editor.c b/src/libtrx/game/ui/dialogs/controls_editor.c new file mode 100644 index 000000000..1bf98943a --- /dev/null +++ b/src/libtrx/game/ui/dialogs/controls_editor.c @@ -0,0 +1,375 @@ +#include "game/ui/dialogs/controls_editor.h" + +#include "config.h" +#include "game/const.h" +#include "game/game_string.h" +#include "game/input.h" +#include "game/shell.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/requester.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "game/ui/elements/window.h" +#include "utils.h" + +typedef enum { + M_PHASE_NAVIGATE_LAYOUT, + M_PHASE_NAVIGATE_INPUTS, + M_PHASE_NAVIGATE_INPUTS_DEBOUNCE, + M_PHASE_LISTEN, + M_PHASE_LISTEN_DEBOUNCE, + M_PHASE_EXIT, +} M_PHASE; + +static const INPUT_ROLE m_LeftRoles[] = { + // clang-format off + INPUT_ROLE_UP, + INPUT_ROLE_DOWN, + INPUT_ROLE_LEFT, + INPUT_ROLE_RIGHT, + INPUT_ROLE_STEP_L, + INPUT_ROLE_STEP_R, + INPUT_ROLE_SLOW, + INPUT_ROLE_ENTER_CONSOLE, + INPUT_ROLE_PAUSE, + INPUT_ROLE_TOGGLE_PHOTO_MODE, + INPUT_ROLE_TOGGLE_UI, + // INPUT_ROLE_CAMERA_RESET, // same as look, no need to configure + INPUT_ROLE_CAMERA_UP, + INPUT_ROLE_CAMERA_DOWN, + INPUT_ROLE_CAMERA_LEFT, + INPUT_ROLE_CAMERA_RIGHT, + INPUT_ROLE_CAMERA_FORWARD, + INPUT_ROLE_CAMERA_BACK, + (INPUT_ROLE)-1, + // clang-format on +}; + +static const INPUT_ROLE m_RightRoles_CheatsOff[] = { + // clang-format off + INPUT_ROLE_JUMP, + INPUT_ROLE_ACTION, + INPUT_ROLE_DRAW, +#if TR_VERSION == 2 + INPUT_ROLE_USE_FLARE, +#endif + INPUT_ROLE_LOOK, + INPUT_ROLE_ROLL, + INPUT_ROLE_OPTION, + (INPUT_ROLE)-1, + // clang-format on +}; + +static const INPUT_ROLE m_RightRoles_CheatsOn[] = { + // clang-format off + INPUT_ROLE_JUMP, + INPUT_ROLE_ACTION, + INPUT_ROLE_DRAW, +#if TR_VERSION == 2 + INPUT_ROLE_USE_FLARE, +#endif + INPUT_ROLE_LOOK, + INPUT_ROLE_ROLL, + INPUT_ROLE_OPTION, + INPUT_ROLE_FLY_CHEAT, + INPUT_ROLE_ITEM_CHEAT, + INPUT_ROLE_LEVEL_SKIP_CHEAT, + INPUT_ROLE_TURBO_CHEAT, + (INPUT_ROLE)-1, + // clang-format on +}; + +static const INPUT_ROLE *m_RightRoles = nullptr; + +static INPUT_ROLE M_GetInputRole(int32_t col, int32_t row); +static int32_t M_GetInputRoleCount(int32_t col); +static void M_CycleLayout(UI_CONTROLS_EDITOR_STATE *s, int32_t dir); +static UI_CONTROLS_CHOICE M_NavigateLayout(UI_CONTROLS_EDITOR_STATE *s); +static UI_CONTROLS_CHOICE M_NavigateInputs(UI_CONTROLS_EDITOR_STATE *s); +static UI_CONTROLS_CHOICE M_NavigateInputsDebounce(UI_CONTROLS_EDITOR_STATE *s); +static UI_CONTROLS_CHOICE M_Listen(UI_CONTROLS_EDITOR_STATE *s); +static UI_CONTROLS_CHOICE M_ListenDebounce(UI_CONTROLS_EDITOR_STATE *s); + +static void M_Title(const UI_CONTROLS_EDITOR_STATE *s); +static void M_InputChoice(UI_CONTROLS_EDITOR_STATE *s, INPUT_ROLE role); +static void M_InputLabel(const UI_CONTROLS_EDITOR_STATE *s, INPUT_ROLE role); +static void M_Column(UI_CONTROLS_EDITOR_STATE *s, const INPUT_ROLE *roles); + +static INPUT_ROLE M_GetInputRole(const int32_t col, const int32_t row) +{ + if (col == 0) { + return m_LeftRoles[row]; + } else { + return m_RightRoles[row]; + } +} + +static int32_t M_GetInputRoleCount(const int32_t col) +{ + int32_t row = 0; + while (M_GetInputRole(col, row) != (INPUT_ROLE)-1) { + row++; + } + return row; +} + +static void M_CycleLayout(UI_CONTROLS_EDITOR_STATE *const s, const int32_t dir) +{ + s->active_layout += dir; + s->active_layout += INPUT_LAYOUT_NUMBER_OF; + s->active_layout %= INPUT_LAYOUT_NUMBER_OF; + + const EVENT event = { + .name = "layout_change", + .sender = nullptr, + .data = nullptr, + }; + EventManager_Fire(s->events, &event); +} + +static UI_CONTROLS_CHOICE M_NavigateLayout(UI_CONTROLS_EDITOR_STATE *const s) +{ + if (g_InputDB.menu_confirm) { + return UI_CONTROLS_CHOICE_EXIT; + } else if (g_InputDB.menu_back) { + return UI_CONTROLS_CHOICE_GO_BACK; + } else if (g_InputDB.menu_left) { + M_CycleLayout(s, -1); + } else if (g_InputDB.menu_right) { + M_CycleLayout(s, 1); + } else if (g_InputDB.menu_down && s->active_layout != 0) { + s->phase = M_PHASE_NAVIGATE_INPUTS; + s->active_col = 0; + s->active_row = 0; + } else if (g_InputDB.menu_up && s->active_layout != 0) { + s->phase = M_PHASE_NAVIGATE_INPUTS; + s->active_col = 1; + s->active_row = M_GetInputRoleCount(1) - 1; + } else { + return UI_CONTROLS_CHOICE_NOOP; + } + s->active_role = M_GetInputRole(s->active_col, s->active_row); + return UI_CONTROLS_CHOICE_NOOP; +} + +static UI_CONTROLS_CHOICE M_NavigateInputs(UI_CONTROLS_EDITOR_STATE *const s) +{ + if (g_InputDB.menu_confirm) { + s->phase = M_PHASE_NAVIGATE_INPUTS_DEBOUNCE; + } else if (g_InputDB.menu_back) { + return UI_CONTROLS_CHOICE_GO_BACK; + } else if (g_InputDB.menu_left || g_InputDB.menu_right) { + s->active_col ^= 1; + CLAMP(s->active_row, 0, M_GetInputRoleCount(s->active_col) - 1); + } else if (g_InputDB.menu_up) { + s->active_row--; + if (s->active_row < 0) { + if (s->active_col == 0) { + s->phase = M_PHASE_NAVIGATE_LAYOUT; + } else { + s->active_col = 0; + s->active_row = M_GetInputRoleCount(0) - 1; + } + } + } else if (g_InputDB.menu_down) { + s->active_row++; + if (s->active_row >= M_GetInputRoleCount(s->active_col)) { + if (s->active_col == 0) { + s->active_col = 1; + s->active_row = 0; + } else { + s->phase = M_PHASE_NAVIGATE_LAYOUT; + } + } + } else { + return UI_CONTROLS_CHOICE_NOOP; + } + s->active_role = M_GetInputRole(s->active_col, s->active_row); + return UI_CONTROLS_CHOICE_NOOP; +} + +static UI_CONTROLS_CHOICE M_NavigateInputsDebounce( + UI_CONTROLS_EDITOR_STATE *const s) +{ + Shell_ProcessEvents(); + Input_Update(); + if (g_Input.any) { + return UI_CONTROLS_CHOICE_NOOP; + } + Input_EnterListenMode(); + s->phase = M_PHASE_LISTEN; + return UI_CONTROLS_CHOICE_NOOP; +} + +static UI_CONTROLS_CHOICE M_Listen(UI_CONTROLS_EDITOR_STATE *const s) +{ + if (!Input_ReadAndAssignRole( + s->backend, s->active_layout, s->active_role)) { + return UI_CONTROLS_CHOICE_NOOP; + } + + Input_ExitListenMode(); + + const EVENT event = { + .name = "key_change", + .sender = nullptr, + .data = nullptr, + }; + EventManager_Fire(s->events, &event); + + s->phase = M_PHASE_LISTEN_DEBOUNCE; + return UI_CONTROLS_CHOICE_NOOP; +} + +static UI_CONTROLS_CHOICE M_ListenDebounce(UI_CONTROLS_EDITOR_STATE *const s) +{ + if (!g_Input.any) { + s->phase = M_PHASE_NAVIGATE_INPUTS; + } + return UI_CONTROLS_CHOICE_NOOP; +} + +static void M_Title(const UI_CONTROLS_EDITOR_STATE *const s) +{ + UI_BeginAnchor(0.5f, 0.5f); + if (s->phase == M_PHASE_NAVIGATE_LAYOUT) { + UI_BeginFrame(UI_FRAME_SELECTED_OPTION); + } + UI_BeginPad(2.0f, 1.0f); + UI_Label(Input_GetLayoutName(s->active_layout)); + UI_EndPad(); + if (s->phase == M_PHASE_NAVIGATE_LAYOUT) { + UI_EndFrame(); + } + UI_EndAnchor(); +} + +static void M_InputLabel( + const UI_CONTROLS_EDITOR_STATE *const s, const INPUT_ROLE role) +{ + const bool is_selected = s->active_role == role + && (s->phase == M_PHASE_NAVIGATE_INPUTS + || s->phase == M_PHASE_NAVIGATE_INPUTS_DEBOUNCE); + if (is_selected) { + UI_BeginFrame(UI_FRAME_SELECTED_OPTION); + } + UI_Label(Input_GetRoleName(role)); + if (is_selected) { + UI_EndFrame(); + } +} + +static void M_InputChoice( + UI_CONTROLS_EDITOR_STATE *const s, const INPUT_ROLE role) +{ + const bool is_flashing = + Input_IsKeyConflicted(s->backend, s->active_layout, role); + const bool is_selected = + s->active_role == role && s->phase == M_PHASE_LISTEN; + + if (is_flashing) { + UI_BeginFlash(&s->flash); + } + if (is_selected) { + UI_BeginFrame(UI_FRAME_SELECTED_OPTION); + } + UI_Label(Input_GetKeyName(s->backend, s->active_layout, role)); + if (is_selected) { + UI_EndFrame(); + } + if (is_flashing) { + UI_EndFlash(); + } +} + +static void M_Column( + UI_CONTROLS_EDITOR_STATE *const s, const INPUT_ROLE *const roles) +{ + UI_BeginStack(UI_STACK_HORIZONTAL); + UI_BeginStack(UI_STACK_VERTICAL); + for (const INPUT_ROLE *role = roles; *role != (INPUT_ROLE)-1; role++) { + M_InputChoice(s, *role); + } + UI_EndStack(); + UI_Spacer(10.0f, 0.0f); + UI_BeginStack(UI_STACK_VERTICAL); + for (const INPUT_ROLE *role = roles; *role != (INPUT_ROLE)-1; role++) { + M_InputLabel(s, *role); + } + UI_EndStack(); + UI_EndStack(); +} + +void UI_ControlsEditor_Init( + UI_CONTROLS_EDITOR_STATE *const s, EVENT_MANAGER *events) +{ + m_RightRoles = g_Config.gameplay.enable_cheats ? m_RightRoles_CheatsOn + : m_RightRoles_CheatsOff; + s->events = events; + UI_Flash_Init(&s->flash, LOGIC_FPS * 2 / 3); +} + +void UI_ControlsEditor_Free(UI_CONTROLS_EDITOR_STATE *const s) +{ + UI_Flash_Free(&s->flash); +} + +void UI_ControlsEditor_Reinit( + UI_CONTROLS_EDITOR_STATE *s, INPUT_BACKEND backend, int32_t layout) +{ + s->backend = backend; + s->active_layout = layout; + s->active_row = 0; + s->active_col = 0; + s->active_role = M_GetInputRole(s->active_col, s->active_row); + s->phase = M_PHASE_NAVIGATE_LAYOUT; +} + +UI_CONTROLS_CHOICE UI_ControlsEditor_Control(UI_CONTROLS_EDITOR_STATE *const s) +{ + UI_Flash_Control(&s->flash); + switch (s->phase) { + case M_PHASE_NAVIGATE_LAYOUT: + return M_NavigateLayout(s); + case M_PHASE_NAVIGATE_INPUTS: + return M_NavigateInputs(s); + case M_PHASE_NAVIGATE_INPUTS_DEBOUNCE: + return M_NavigateInputsDebounce(s); + case M_PHASE_LISTEN: + return M_Listen(s); + case M_PHASE_LISTEN_DEBOUNCE: + return M_ListenDebounce(s); + default: + return UI_CONTROLS_CHOICE_NOOP; + } +} + +void UI_ControlsEditor(UI_CONTROLS_EDITOR_STATE *const s) +{ + UI_BeginModal(0.5f, 0.5f); + UI_BeginWindow(); + UI_WindowTitle(GS(CONTROLS_CUSTOMIZE)); + UI_BeginWindowBody(); + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + }); + M_Title(s); + UI_Spacer(0.0f, 5.0f); + + UI_BeginStack(UI_STACK_HORIZONTAL); + M_Column(s, m_LeftRoles); + UI_Spacer(10.0f, 0.0f); + M_Column(s, m_RightRoles); + UI_EndStack(); + + UI_EndStack(); + UI_EndWindowBody(); + UI_EndWindow(); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/examine_item.c b/src/libtrx/game/ui/dialogs/examine_item.c new file mode 100644 index 000000000..19256eed6 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/examine_item.c @@ -0,0 +1,144 @@ +#include "game/ui/dialogs/examine_item.h" + +#include "game/game_string.h" +#include "game/input.h" +#include "game/sound.h" +#include "game/text.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/resize.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "memory.h" +#include "strings.h" +#include "utils.h" + +#include + +#define TITLE_MARGIN 5.0f +#define WINDOW_MARGIN 10.0f +#define DIALOG_PADDING 8.0f +#define PADDING_SCALED (3.5f * (DIALOG_PADDING + WINDOW_MARGIN)) + +static bool M_SelectPage(UI_EXAMINE_ITEM_STATE *state, int32_t new_page); + +static bool M_SelectPage( + UI_EXAMINE_ITEM_STATE *const state, const int32_t new_page) +{ + if (new_page == state->current_page || new_page < 0 + || new_page >= state->page_content->count) { + return false; + } + state->current_page = new_page; + return true; +} + +void UI_ExamineItem_Init( + UI_EXAMINE_ITEM_STATE *const state, const char *const title, + const char *const text, size_t max_lines) +{ + state->current_page = 0; + state->title = String_ToUpper(title); + + const char *wrapped = + String_WordWrap(text, Text_GetMaxLineLength() - PADDING_SCALED); + state->page_content = String_Paginate(wrapped, max_lines); + state->is_empty = String_IsEmpty(text); + Memory_FreePointer(&wrapped); + + // Compute actual maximum number of lines on any single page, which can be + // less than the line cap. + size_t max_vis_lines = 0; + for (int32_t i = 0; i < state->page_content->count; i++) { + size_t page_lines = 1; + const char *c = *(char **)Vector_Get(state->page_content, i); + while (*c != '\0') { + page_lines += *c++ == '\n'; + } + CLAMPL(max_vis_lines, page_lines); + } + state->max_lines = max_vis_lines; +} + +void UI_ExamineItem_Control(UI_EXAMINE_ITEM_STATE *const state) +{ + const int32_t page_shift = + g_InputDB.menu_left ? -1 : (g_InputDB.menu_right ? 1 : 0); + if (M_SelectPage(state, state->current_page + page_shift)) { + Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); + } +} + +void UI_ExamineItem_Free(UI_EXAMINE_ITEM_STATE *const state) +{ + Memory_FreePointer(&state->title); + for (int32_t i = state->page_content->count - 1; i >= 0; i--) { + char *const page = *(char **)Vector_Get(state->page_content, i); + Memory_Free(page); + } + Vector_Free(state->page_content); +} + +void UI_ExamineItem(UI_EXAMINE_ITEM_STATE *const state) +{ + if (state->is_empty) { + return; + } + + UI_BeginModal(0.5f, 0.5f); + UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND); + UI_BeginPad(DIALOG_PADDING, DIALOG_PADDING); + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + }); + + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(state->title); + UI_EndAnchor(); + UI_Spacer(TITLE_MARGIN, TITLE_MARGIN); + + for (int32_t i = 0; i < state->page_content->count; i++) { + if (i != state->current_page) { + UI_BeginResize(-1.0f, 0.0f); + } else if (state->page_content->count == 1) { + UI_BeginResize(-1.0f, -1.0f); + } else { + UI_BeginResize(-1.0f, TEXT_HEIGHT_FIXED * state->max_lines); + } + UI_Label(*(char **)Vector_Get(state->page_content, i)); + UI_EndResize(); + } + + if (state->page_content->count > 1) { + UI_Spacer(TITLE_MARGIN, TITLE_MARGIN * 3); + + UI_BeginAnchor(1.0f, 0.5f); + UI_BeginStack(UI_STACK_HORIZONTAL); + + if (state->current_page > 0) { + UI_Label("\\{button left} "); + } + + char page_indicator[100]; + sprintf( + page_indicator, GS(PAGINATION_NAV), state->current_page + 1, + state->page_content->count); + UI_Label(page_indicator); + + if (state->current_page < state->page_content->count - 1) { + UI_Label(" \\{button right}"); + } + + UI_EndStack(); + UI_EndAnchor(); + } + + UI_EndStack(); + UI_EndPad(); + UI_EndFrame(); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/new_game.c b/src/libtrx/game/ui/dialogs/new_game.c new file mode 100644 index 000000000..694be289b --- /dev/null +++ b/src/libtrx/game/ui/dialogs/new_game.c @@ -0,0 +1,55 @@ +#include "game/ui/dialogs/new_game.h" + +#include "game/game_string.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/requester.h" + +static const GAME_STRING_ID m_Options[] = { + GS_ID(PASSPORT_MODE_NEW_GAME), + GS_ID(PASSPORT_MODE_NEW_GAME_PLUS), + GS_ID(PASSPORT_MODE_NEW_GAME_JP), + GS_ID(PASSPORT_MODE_NEW_GAME_JP_PLUS), + nullptr, +}; + +void UI_NewGame_Init(UI_NEW_GAME_STATE *const s) +{ + int32_t option_count = 0; + for (int32_t i = 0; m_Options[i] != nullptr; i++) { + option_count++; + } + + UI_Requester_Init(&s->req, option_count, option_count, true); +} + +void UI_NewGame_Free(UI_NEW_GAME_STATE *const s) +{ + UI_Requester_Free(&s->req); +} + +int32_t UI_NewGame_Control(UI_NEW_GAME_STATE *const s) +{ + return UI_Requester_Control(&s->req); +} + +void UI_NewGame(UI_NEW_GAME_STATE *const s) +{ + UI_BeginModal(0.5f, 2.0f / 3.0f); + UI_BeginRequester(&s->req, GS(PASSPORT_SELECT_MODE)); + + for (int32_t i = UI_Requester_GetFirstRow(&s->req); + i < UI_Requester_GetLastRow(&s->req); i++) { + UI_BeginRequesterRow(&s->req, i); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(GameString_Get(m_Options[i])); + UI_EndAnchor(); + UI_EndRequesterRow(&s->req, i); + } + + UI_EndRequester(&s->req); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/pause.c b/src/libtrx/game/ui/dialogs/pause.c new file mode 100644 index 000000000..b329ca1b1 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/pause.c @@ -0,0 +1,72 @@ +#include "game/ui/dialogs/pause.h" + +#include "game/game_string.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/requester.h" + +static const GAME_STRING_ID m_Options[2][2] = { + { GS_ID(PAUSE_CONTINUE), GS_ID(PAUSE_QUIT) }, + { GS_ID(PAUSE_YES), GS_ID(PAUSE_NO) }, +}; + +void UI_Pause_Init(UI_PAUSE_STATE *const s) +{ + s->phase = 0; + UI_Requester_Init(&s->req, 2, 2, true); +} + +void UI_Pause_Free(UI_PAUSE_STATE *const s) +{ + UI_Requester_Free(&s->req); +} + +UI_PAUSE_EXIT_CHOICE UI_Pause_Control(UI_PAUSE_STATE *const s) +{ + const int32_t choice = UI_Requester_Control(&s->req); + if (s->phase == 0) { + if (choice == UI_REQUESTER_CANCEL) { + return UI_PAUSE_RESUME_PAUSE; + } else if (choice == 0) { + return UI_PAUSE_EXIT_TO_GAME; + } else if (choice == 1) { + s->phase = 1; + UI_Requester_Free(&s->req); + UI_Requester_Init(&s->req, 2, 2, true); + } + } else { + if (choice == UI_REQUESTER_CANCEL) { + s->phase = 0; + } else if (choice == 0) { + return UI_PAUSE_EXIT_TO_TITLE; + } else if (choice == 1) { + return UI_PAUSE_EXIT_TO_GAME; + } + } + return UI_PAUSE_NOOP; +} + +void UI_Pause(UI_PAUSE_STATE *const s) +{ + UI_BeginModal(0.5f, 1.0f); + UI_BeginPad(50.0f, 50.0f); + UI_BeginRequester( + &s->req, + s->phase == 0 ? GS(PAUSE_EXIT_TO_TITLE) : GS(PAUSE_ARE_YOU_SURE)); + + for (int32_t i = UI_Requester_GetFirstRow(&s->req); + i < UI_Requester_GetLastRow(&s->req); i++) { + UI_BeginRequesterRow(&s->req, i); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(GameString_Get(m_Options[s->phase][i])); + UI_EndAnchor(); + UI_EndRequesterRow(&s->req, i); + } + + UI_EndRequester(&s->req); + UI_EndPad(); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/photo_mode.c b/src/libtrx/game/ui/dialogs/photo_mode.c new file mode 100644 index 000000000..512f4b3e1 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/photo_mode.c @@ -0,0 +1,111 @@ +#include "game/ui/dialogs/photo_mode.h" + +#include "config.h" +#include "game/game_string.h" +#include "game/input.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" + +#include + +void UI_PhotoMode(void) +{ + if (!g_Config.ui.enable_photo_mode_ui) { + return; + } + + char tmp[50]; + + UI_BeginModal(0.0f, 0.0f); + UI_BeginPad(8.0f, 10.0f); + UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND); + UI_BeginPad(8.0, 6.0); + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .spacing = { .v = 8.0f }, + }); + UI_Label(GS(PHOTO_MODE_TITLE)); + + UI_BeginStack(UI_STACK_HORIZONTAL); + + // Inputs column + UI_BeginStack(UI_STACK_VERTICAL); + sprintf( + tmp, "%s%s%s%s%s%s: ", + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_CAMERA_UP), + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_CAMERA_DOWN), + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_CAMERA_FORWARD), + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_CAMERA_BACK), + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_CAMERA_LEFT), + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_CAMERA_RIGHT)); + UI_Label(tmp); + UI_Label( + "\\{button left} \\{button up} " + "\\{button down} \\{button right} : "); + sprintf(tmp, "%s: ", GS(PHOTO_MODE_ROLL_ROLE)); + UI_Label(tmp); + sprintf(tmp, "%s: ", GS(KEYMAP_ROLL)); + UI_Label(tmp); + sprintf(tmp, "%s: ", GS(PHOTO_MODE_FOV_ROLE)); + UI_Label(tmp); + sprintf(tmp, "%s: ", GS(KEYMAP_LOOK)); + UI_Label(tmp); + sprintf( + tmp, "%s: ", + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_TOGGLE_UI)); + UI_Label(tmp); + sprintf(tmp, "%s: ", GS(KEYMAP_ACTION)); + UI_Label(tmp); + sprintf( + tmp, "%s/%s: ", + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_TOGGLE_PHOTO_MODE), + Input_GetKeyName( + INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, + INPUT_ROLE_OPTION)); + UI_Label(tmp); + UI_EndStack(); + + UI_Spacer(4.0f, 0.0f); + + // Behaviors column + UI_BeginStack(UI_STACK_VERTICAL); + UI_Label(GS(PHOTO_MODE_MOVE_PROMPT)); + UI_Label(GS(PHOTO_MODE_ROTATE_PROMPT)); + UI_Label(GS(PHOTO_MODE_ROLL_PROMPT)); + UI_Label(GS(PHOTO_MODE_ROTATE90_PROMPT)); + UI_Label(GS(PHOTO_MODE_FOV_PROMPT)); + UI_Label(GS(PHOTO_MODE_RESET_PROMPT)); + UI_Label(GS(MISC_TOGGLE_HELP)); + UI_Label(GS(PHOTO_MODE_SNAP_PROMPT)); + UI_Label(GS(MISC_EXIT)); + UI_EndStack(); + + UI_EndStack(); + + UI_EndStack(); + UI_EndPad(); + UI_EndFrame(); + UI_EndPad(); + UI_EndModal(); +} diff --git a/src/libtrx/game/ui/dialogs/play_any_level.c b/src/libtrx/game/ui/dialogs/play_any_level.c new file mode 100644 index 000000000..d21061f49 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/play_any_level.c @@ -0,0 +1,87 @@ +#include "game/ui/dialogs/play_any_level.h" + +#include "game/game_flow.h" +#include "game/game_string.h" +#include "game/ui/common.h" +#include "game/ui/dialogs/base_passport.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/hide.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/offset.h" +#include "game/ui/elements/requester.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "memory.h" +#include "vector.h" + +typedef struct { + int32_t level_num; + const char *text; +} M_ROW; + +typedef struct UI_PLAY_ANY_LEVEL_DIALOG_STATE { + VECTOR *rows; + UI_REQUESTER_STATE req; +} UI_PLAY_ANY_LEVEL_DIALOG_STATE; + +UI_PLAY_ANY_LEVEL_DIALOG_STATE *UI_PlayAnyLevelDialog_Init(void) +{ + UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s = + Memory_Alloc(sizeof(UI_PLAY_ANY_LEVEL_DIALOG_STATE)); + s->rows = Vector_Create(sizeof(M_ROW)); + + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); + for (int32_t i = 0; i < level_table->count; i++) { + if (level_table->levels[i].type != GFL_GYM) { + const M_ROW row = { + .level_num = i, + .text = level_table->levels[i].title, + }; + Vector_Add(s->rows, &row); + } + } + + UI_BasePassportDialog_Init(&s->req, s->rows->count); + return s; +} + +void UI_PlayAnyLevelDialog_Free(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s) +{ + Vector_Free(s->rows); + UI_Requester_Free(&s->req); + Memory_Free(s); +} + +int32_t UI_PlayAnyLevelDialog_Control(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s) +{ + UI_BasePassportDialog_Control(&s->req); + const int32_t choice = UI_Requester_Control(&s->req); + switch (choice) { + case UI_REQUESTER_NO_CHOICE: + return UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE; + case UI_REQUESTER_CANCEL: + return UI_PLAY_ANY_LEVEL_CHOICE_CANCEL; + default: + return ((M_ROW *)Vector_Get(s->rows, choice))->level_num; + } +} + +void UI_PlayAnyLevelDialog(UI_PLAY_ANY_LEVEL_DIALOG_STATE *const s) +{ + UI_BeginBasePassportDialog(); + UI_BeginRequester(&s->req, GS(PASSPORT_SELECT_LEVEL)); + + for (int32_t i = 0; i < s->rows->count; i++) { + if (UI_Requester_IsRowVisible(&s->req, i)) { + const M_ROW *const row = Vector_Get(s->rows, i); + UI_BeginRequesterRow(&s->req, i); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(row->text); + UI_EndAnchor(); + UI_EndRequesterRow(&s->req, i); + } + } + + UI_EndRequester(&s->req); + UI_EndBasePassportDialog(); +} diff --git a/src/libtrx/game/ui/dialogs/save_slot.c b/src/libtrx/game/ui/dialogs/save_slot.c new file mode 100644 index 000000000..f20eb0d9e --- /dev/null +++ b/src/libtrx/game/ui/dialogs/save_slot.c @@ -0,0 +1,184 @@ +#include "game/ui/dialogs/save_slot.h" + +#include "game/game_string.h" +#include "game/input.h" +#include "game/savegame.h" +#include "game/scaler.h" +#include "game/ui/common.h" +#include "game/ui/dialogs/base_passport.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/hide.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/offset.h" +#include "game/ui/elements/requester.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "game/viewport.h" +#include "memory.h" +#include "utils.h" + +#include + +typedef struct UI_SAVE_SLOT_DIALOG_STATE { + UI_SAVE_SLOT_DIALOG_TYPE type; + UI_REQUESTER_STATE req; +} UI_SAVE_SLOT_DIALOG_STATE; + +static bool M_ShowDetails(const UI_SAVE_SLOT_DIALOG_STATE *s, int32_t slot_idx); +static void M_NonEmptySlot( + const UI_SAVE_SLOT_DIALOG_STATE *s, int32_t slot_idx, + const SAVEGAME_INFO *info); +static void M_EmptySlot(const UI_SAVE_SLOT_DIALOG_STATE *s, int32_t slot_idx); + +static bool M_ShowDetails( + const UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t slot_idx) +{ + if (s->type != UI_SAVE_SLOT_DIALOG_LOAD_GAME) { + return false; + } + if (!UI_Requester_IsRowSelected(&s->req, slot_idx)) { + return false; + } + if (TR_VERSION == 2) { + return false; + } + return !Savegame_IsSlotFree(slot_idx); +} + +static void M_NonEmptySlot( + const UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t slot_idx, + const SAVEGAME_INFO *const info) +{ + const bool show_details = M_ShowDetails(s, slot_idx); + + if (show_details) { + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + }); + // Balance both sides so that the row text appears centered + UI_BeginHide(true); + UI_Label("\\{button right}"); + UI_EndHide(); + if (TR_VERSION == 1) { + UI_BeginStack(UI_STACK_HORIZONTAL); + } + } else { + if (TR_VERSION == 1) { + UI_BeginAnchor(0.5f, 0.5f); + UI_BeginStack(UI_STACK_HORIZONTAL); + } else { + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + }); + } + } + + // Level title with the save counter + UI_Label(info->level_title); + if (info->counter > 0) { + UI_Spacer(8.0f, 0.0f); + char buf[16]; + sprintf(buf, "%d", info->counter); + UI_Label(buf); + } + + if (show_details) { + if (TR_VERSION == 1) { + UI_EndStack(); + } + UI_BeginOffset(0.0f, -1.0f); + UI_Label("\\{button right}"); + UI_EndOffset(); + UI_EndStack(); + } else { + UI_EndStack(); + if (TR_VERSION == 1) { + UI_EndAnchor(); + } + } +} + +static void M_EmptySlot( + const UI_SAVE_SLOT_DIALOG_STATE *const s, const int32_t slot_idx) +{ + char buf[16]; + sprintf(buf, GS(MISC_EMPTY_SLOT_FMT), slot_idx + 1); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(buf); + UI_EndAnchor(); +} + +UI_SAVE_SLOT_DIALOG_STATE *UI_SaveSlotDialog_Init( + const UI_SAVE_SLOT_DIALOG_TYPE type, const int32_t save_slot) +{ + UI_SAVE_SLOT_DIALOG_STATE *const s = + Memory_Alloc(sizeof(UI_SAVE_SLOT_DIALOG_STATE)); + s->type = type; + + UI_BasePassportDialog_Init(&s->req, Savegame_GetSlotCount()); + s->req.sel_row = save_slot; + CLAMP(s->req.sel_row, 0, s->req.max_rows); + return s; +} + +void UI_SaveSlotDialog_Free(UI_SAVE_SLOT_DIALOG_STATE *const s) +{ + UI_Requester_Free(&s->req); +} + +UI_SAVE_SLOT_DIALOG_CHOICE UI_SaveSlotDialog_Control( + UI_SAVE_SLOT_DIALOG_STATE *const s) +{ + UI_BasePassportDialog_Control(&s->req); + const int32_t sel_row = UI_Requester_GetCurrentRow(&s->req); + if (M_ShowDetails(s, sel_row) && g_InputDB.menu_right) { + return (UI_SAVE_SLOT_DIALOG_CHOICE) { + .action = UI_SAVE_SLOT_DIALOG_DETAILS, + .slot_num = sel_row, + }; + } + const int32_t choice = UI_Requester_Control(&s->req); + if (choice == UI_REQUESTER_CANCEL) { + return (UI_SAVE_SLOT_DIALOG_CHOICE) { + .action = UI_SAVE_SLOT_DIALOG_CANCEL, + }; + } else if ( + choice != UI_REQUESTER_NO_CHOICE + && (s->type == UI_SAVE_SLOT_DIALOG_SAVE_GAME + || !Savegame_IsSlotFree(choice))) { + return (UI_SAVE_SLOT_DIALOG_CHOICE) { + .action = UI_SAVE_SLOT_DIALOG_CONFIRM, + .slot_num = sel_row, + }; + } + return (UI_SAVE_SLOT_DIALOG_CHOICE) { + .action = UI_SAVE_SLOT_DIALOG_NO_CHOICE, + }; +} + +void UI_SaveSlotDialog(const UI_SAVE_SLOT_DIALOG_STATE *const s) +{ + UI_BeginBasePassportDialog(); + const char *const title = (s->type == UI_SAVE_SLOT_DIALOG_SAVE_GAME) + ? GS(PASSPORT_SAVE_GAME) + : GS(PASSPORT_LOAD_GAME); + UI_BeginRequester(&s->req, title); + + const int32_t first = UI_Requester_GetFirstRow(&s->req); + const int32_t last = UI_Requester_GetLastRow(&s->req); + for (int32_t i = first; i < last; ++i) { + UI_BeginRequesterRow(&s->req, i); + const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(i); + if (info != nullptr && info->level_title != nullptr) { + M_NonEmptySlot(s, i, info); + } else { + M_EmptySlot(s, i); + } + UI_EndRequesterRow(&s->req, i); + } + + UI_EndRequester(&s->req); + UI_EndBasePassportDialog(); +} diff --git a/src/libtrx/game/ui/dialogs/select_level.c b/src/libtrx/game/ui/dialogs/select_level.c new file mode 100644 index 000000000..f2db94fc9 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/select_level.c @@ -0,0 +1,140 @@ +#include "game/ui/dialogs/select_level.h" + +#include "debug.h" +#include "game/game_flow.h" +#include "game/game_string.h" +#include "game/inventory.h" +#include "game/savegame.h" +#include "game/ui/common.h" +#include "game/ui/dialogs/base_passport.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/hide.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/offset.h" +#include "game/ui/elements/requester.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "memory.h" +#include "vector.h" + +typedef enum { + M_ROW_ROLE_PLAY_LEVEL, + M_ROW_ROLE_STORY_SO_FAR, +} M_ROW_ROLE; + +typedef struct { + const char *const text; + M_ROW_ROLE role; +} M_ROW; + +typedef struct UI_SELECT_LEVEL_DIALOG_STATE { + int32_t save_slot; + bool is_active; + VECTOR *rows; + UI_REQUESTER_STATE req; +} UI_SELECT_LEVEL_DIALOG_STATE; + +UI_SELECT_LEVEL_DIALOG_STATE *UI_SelectLevelDialog_Init(const int32_t save_slot) +{ + UI_SELECT_LEVEL_DIALOG_STATE *const s = + Memory_Alloc(sizeof(UI_SELECT_LEVEL_DIALOG_STATE)); + s->save_slot = save_slot; + s->rows = Vector_Create(sizeof(M_ROW)); + + const SAVEGAME_INFO *const info = Savegame_GetSavegameInfo(save_slot); + ASSERT(info != nullptr); + s->is_active = info->features.select_level; + + if (s->is_active) { + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); + for (int32_t i = 0; i <= info->level_num && i < level_table->count; + i++) { + if (level_table->levels[i].type != GFL_GYM) { + Vector_Add( + s->rows, + &(M_ROW) { + .text = level_table->levels[i].title, + .role = M_ROW_ROLE_PLAY_LEVEL, + }); + } + } + + if (g_Inv_Mode == INV_TITLE_MODE && GF_HasAvailableStory(save_slot)) { + Vector_Add( + s->rows, + &(M_ROW) { + .text = GS(PASSPORT_STORY_SO_FAR), + .role = M_ROW_ROLE_STORY_SO_FAR, + }); + } + } + + UI_BasePassportDialog_Init(&s->req, s->rows->count); + return s; +} + +void UI_SelectLevelDialog_Free(UI_SELECT_LEVEL_DIALOG_STATE *const s) +{ + Vector_Free(s->rows); + UI_Requester_Free(&s->req); + Memory_Free(s); +} + +int32_t UI_SelectLevelDialog_Control(UI_SELECT_LEVEL_DIALOG_STATE *const s) +{ + UI_BasePassportDialog_Control(&s->req); + const int32_t choice = UI_Requester_Control(&s->req); + if (choice < 0 || !s->is_active) { + return UI_SELECT_LEVEL_CHOICE_NOOP; + } + const M_ROW *const row = Vector_Get(s->rows, choice); + if (row->role == M_ROW_ROLE_STORY_SO_FAR) { + return UI_SELECT_LEVEL_CHOICE_PLAY_STORY_SO_FAR; + } + return choice; +} + +void UI_SelectLevelDialog(UI_SELECT_LEVEL_DIALOG_STATE *const s) +{ + UI_BeginBasePassportDialog(); + UI_BeginRequester(&s->req, GS(PASSPORT_SELECT_LEVEL)); + + const SAVEGAME_INFO *info = Savegame_GetSavegameInfo(s->save_slot); + if (!s->is_active) { + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(GS(PASSPORT_LEGACY_SELECT_LEVEL_1)); + UI_EndAnchor(); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(GS(PASSPORT_LEGACY_SELECT_LEVEL_2)); + UI_EndAnchor(); + } else { + for (int32_t i = 0; i < s->rows->count; i++) { + if (UI_Requester_IsRowVisible(&s->req, i)) { + const M_ROW *const row = Vector_Get(s->rows, i); + UI_BeginRequesterRow(&s->req, i); + if (UI_Requester_IsRowSelected(&s->req, i)) { + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE } }); + UI_BeginOffset(0.0f, -1.0f); + UI_Label("\\{button left}"); + UI_EndOffset(); + UI_Label(row->text); + // balance both sides so that the row text appears centered + UI_BeginHide(true); + UI_Label("\\{button left}"); + UI_EndHide(); + UI_EndStack(); + } else { + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(row->text); + UI_EndAnchor(); + } + UI_EndRequesterRow(&s->req, i); + } + } + } + + UI_EndRequester(&s->req); + UI_EndBasePassportDialog(); +} diff --git a/src/libtrx/game/ui/dialogs/stats.c b/src/libtrx/game/ui/dialogs/stats.c new file mode 100644 index 000000000..84982c734 --- /dev/null +++ b/src/libtrx/game/ui/dialogs/stats.c @@ -0,0 +1,28 @@ +#include "game/ui/dialogs/stats.h" + +#include "game/gym.h" + +void UI_StatsDialog_Init( + UI_STATS_DIALOG_STATE *const s, const UI_STATS_DIALOG_ARGS args) +{ + const ASSAULT_STATS stats = Gym_GetAssaultStats(); + int32_t max_assault_times = 0; + for (int i = 0; i < MAX_ASSAULT_TIMES; i++) { + if (stats.entries[i].time != 0) { + max_assault_times++; + } + } + UI_Requester_Init(&s->assault_req, 7, max_assault_times, false); + s->assault_req.reserve_space = true; + s->args = args; +} + +void UI_StatsDialog_Free(UI_STATS_DIALOG_STATE *const s) +{ + UI_Requester_Free(&s->assault_req); +} + +int32_t UI_StatsDialog_Control(UI_STATS_DIALOG_STATE *const s) +{ + return UI_Requester_Control(&s->assault_req); +} diff --git a/src/libtrx/game/ui/elements/anchor.c b/src/libtrx/game/ui/elements/anchor.c new file mode 100644 index 000000000..57170dab6 --- /dev/null +++ b/src/libtrx/game/ui/elements/anchor.c @@ -0,0 +1,56 @@ +#include "game/ui/elements/anchor.h" + +#include "game/text.h" +#include "game/ui/helpers.h" + +typedef struct { + float x; + float y; +} M_DATA; + +static void M_Measure(UI_NODE *node); +static void M_Layout(UI_NODE *node, float x, float y, float w, float h); + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = M_Layout, + .draw = UI_DrawWrapper, +}; + +static void M_Measure(UI_NODE *const node) +{ + node->measure_w = UI_GetCanvasWidth(); + node->measure_h = UI_GetCanvasHeight() - TEXT_HEIGHT_FIXED; +} + +static void M_Layout( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + UI_LayoutBasic(node, x, y, w, h); + const M_DATA *const data = node->data; + UI_NODE *child = node->first_child; + while (child != nullptr) { + const float cw = child->measure_w; + const float ch = child->measure_h; + const float cx = x + (w - cw) * data->x; + const float cy = y + (h - ch) * data->y; + child->ops->layout(child, cx, cy, cw, ch); + child = child->next_sibling; + } +} + +void UI_BeginAnchor(const float x, const float y) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + data->x = x; + data->y = y; + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndAnchor(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/fade.c b/src/libtrx/game/ui/elements/fade.c new file mode 100644 index 000000000..3e04dd5c6 --- /dev/null +++ b/src/libtrx/game/ui/elements/fade.c @@ -0,0 +1,45 @@ +#include "game/ui/elements/fade.h" + +#include "game/output.h" +#include "game/ui/helpers.h" + +typedef struct { + FADER *fader; + bool is_on_top; +} M_DATA; + +static void M_Draw(const UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = UI_LayoutWrapper, + .draw = M_Draw, +}; + +static void M_Draw(const UI_NODE *const node) +{ + const M_DATA *const data = node->data; + if (data->is_on_top) { + UI_DrawWrapper(node); + Output_DrawPolyList(); // flush geometry + Fader_Draw(data->fader); + } else { + Fader_Draw(data->fader); + UI_DrawWrapper(node); + } +} + +void UI_BeginFade(FADER *const fader, bool is_on_top) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + data->fader = fader; + data->is_on_top = is_on_top; + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndFade(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/fixed.c b/src/libtrx/game/ui/elements/fixed.c new file mode 100644 index 000000000..d20208771 --- /dev/null +++ b/src/libtrx/game/ui/elements/fixed.c @@ -0,0 +1,67 @@ +#include "game/ui/elements/fixed.h" + +#include "game/ui/helpers.h" +#include "utils.h" + +typedef struct { + float x; + float y; +} M_DATA; + +static void M_Measure(UI_NODE *node); +static void M_Layout(UI_NODE *node, float x, float y, float w, float h); +static void M_Draw(const UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = M_Layout, + .draw = M_Draw, +}; + +static void M_Measure(UI_NODE *const node) +{ + node->measure_w = 0.0f; + node->measure_h = 0.0f; +} + +static void M_Layout(UI_NODE *const node, float x, float y, float w, float h) +{ + const M_DATA *const data = node->data; + w = 0; + h = 0; + const UI_NODE *child = node->first_child; + while (child != nullptr) { + w = MAX(w, child->measure_w); + h = MAX(h, child->measure_h); + child = child->next_sibling; + } + x -= w * data->x; + y -= h * data->y; + UI_LayoutWrapper(node, x, y, w, h); +} + +static void M_Draw(const UI_NODE *const node) +{ + const UI_NODE *child = node->first_child; + while (child != nullptr) { + if (child->ops->draw != nullptr) { + child->ops->draw(child); + } + child = child->next_sibling; + } +} + +void UI_BeginFixed(const float x, const float y) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + data->x = x; + data->y = y; + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndFixed(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/flash.c b/src/libtrx/game/ui/elements/flash.c new file mode 100644 index 000000000..e4b52d92a --- /dev/null +++ b/src/libtrx/game/ui/elements/flash.c @@ -0,0 +1,43 @@ +#include "game/ui/elements/flash.h" + +#include "game/ui/elements/hide.h" +#include "game/ui/helpers.h" + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = UI_LayoutWrapper, + .draw = UI_DrawWrapper, +}; + +void UI_Flash_Init(UI_FLASH_STATE *const s, const int32_t rate) +{ + s->count = 0; + s->rate = rate; +} + +void UI_Flash_Free(UI_FLASH_STATE *const s) +{ +} + +void UI_Flash_Control(UI_FLASH_STATE *const s) +{ + s->count -= 2; + if (s->count < -s->rate) { + s->count = s->rate; + } +} + +void UI_BeginFlash(UI_FLASH_STATE *const s) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(UI_FLASH_STATE *)); + *(UI_FLASH_STATE **)node->data = s; + UI_AddChild(node); + UI_PushCurrent(node); + UI_BeginHide(s->count >= 0); +} + +void UI_EndFlash(void) +{ + UI_EndHide(); + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/frame.c b/src/libtrx/game/ui/elements/frame.c new file mode 100644 index 000000000..2b78d163f --- /dev/null +++ b/src/libtrx/game/ui/elements/frame.c @@ -0,0 +1,81 @@ +#include "game/ui/elements/frame.h" + +#include "config.h" +#include "game/output.h" +#include "game/text.h" +#include "game/ui/helpers.h" + +typedef struct { + UI_STYLE ui_style; + TEXT_STYLE text_style; + int32_t outline_z; + int32_t background_z; +} M_DATA; + +static void M_Draw(const UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = UI_LayoutWrapper, + .draw = M_Draw, +}; + +static void M_Draw(const UI_NODE *node) +{ + const M_DATA *const data = node->data; + if (data->background_z >= 0) { + Output_DrawTextBackground( + data->ui_style, UI_ScaleX(node->x), UI_ScaleY(node->y), + UI_ScaleX(node->w), UI_ScaleY(node->h), data->background_z, + data->text_style); + } + if (data->outline_z >= 0) { + Output_DrawTextOutline( + data->ui_style, UI_ScaleX(node->x), UI_ScaleY(node->y), + UI_ScaleX(node->w), UI_ScaleY(node->h), data->outline_z, + data->text_style); + } + UI_DrawWrapper(node); +} + +void UI_BeginFrame(UI_FRAME_STYLE style) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + + data->ui_style = UI_STYLE_PC; +#if TR_VERSION == 1 + data->ui_style = g_Config.ui.menu_style; +#endif + + switch (style) { + case UI_FRAME_DIALOG_BACKGROUND: + data->outline_z = 160; + data->background_z = 160; + data->text_style = TS_BACKGROUND; + break; + case UI_FRAME_DIALOG_HEADING: + data->outline_z = 80; + data->background_z = 80; + data->text_style = TS_HEADING; + break; + case UI_FRAME_SELECTED_OPTION: + data->outline_z = 80; + data->background_z = 80; + data->text_style = TS_REQUESTED; + break; + case UI_FRAME_OUTLINE_ONLY: + data->outline_z = 80; + data->background_z = -1; + data->text_style = TS_REQUESTED; + break; + } + + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndFrame(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/hide.c b/src/libtrx/game/ui/elements/hide.c new file mode 100644 index 000000000..2e03be1cc --- /dev/null +++ b/src/libtrx/game/ui/elements/hide.c @@ -0,0 +1,32 @@ +#include "game/ui/elements/hide.h" + +#include "game/ui/helpers.h" + +static void M_Draw(const UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = UI_LayoutWrapper, + .draw = M_Draw, +}; + +static void M_Draw(const UI_NODE *const node) +{ + const bool draw_children = *(bool *)node->data; + if (draw_children) { + UI_DrawWrapper(node); + } +} + +void UI_BeginHide(const bool hide_children) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(bool)); + *(bool *)node->data = !hide_children; + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndHide(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/label.c b/src/libtrx/game/ui/elements/label.c new file mode 100644 index 000000000..edf01ff24 --- /dev/null +++ b/src/libtrx/game/ui/elements/label.c @@ -0,0 +1,100 @@ +#include "game/ui/elements/label.h" + +#include "config.h" +#include "game/text.h" +#include "game/ui/helpers.h" + +#include + +typedef struct { + UI_LABEL_SETTINGS settings; + char *text; +} M_DATA; + +static void M_Measure(UI_NODE *node); +static void M_Draw(const UI_NODE *node); + +static UI_LABEL_SETTINGS m_DefaultSettings = { .scale = 1.0f }; +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = UI_LayoutBasic, + .draw = M_Draw, +}; + +static TEXTSTRING *M_CreateText( + const float x, const float y, const char *text, + const UI_LABEL_SETTINGS settings); + +static TEXTSTRING *M_CreateText( + const float x, const float y, const char *text, + const UI_LABEL_SETTINGS settings) +{ + TEXTSTRING *const textstring = Text_Create(x, y, text); + Text_SetPos( + textstring, x / g_Config.ui.text_scale, + y / g_Config.ui.text_scale + TEXT_HEIGHT_FIXED - 1); + Text_SetMultiline(textstring, true); + Text_SetScale( + textstring, settings.scale * TEXT_BASE_SCALE, + settings.scale * TEXT_BASE_SCALE); + return textstring; +} + +static void M_Measure(UI_NODE *const node) +{ + M_DATA *const data = node->data; + UI_Label_MeasureEx( + data->text, &node->measure_w, &node->measure_h, data->settings); +} + +static void M_Draw(const UI_NODE *const node) +{ + M_DATA *const data = node->data; + TEXTSTRING *const textstring = + M_CreateText(node->x, node->y, data->text, data->settings); + if (data->settings.z != 0) { + textstring->pos.z = data->settings.z; + } + Text_DrawText(textstring); + Text_Remove(textstring); + UI_DrawWrapper(node); +} + +void UI_Label(const char *const text) +{ + UI_LabelEx(text, m_DefaultSettings); +} + +void UI_LabelEx(const char *text, const UI_LABEL_SETTINGS settings) +{ + if (text == nullptr) { + text = "(null)"; // quality of life for UI development + } + UI_NODE *const node = + UI_AllocNode(&m_Ops, sizeof(M_DATA) + strlen(text) + 1); + M_DATA *const data = node->data; + data->settings = settings; + data->text = (char *)node->data + sizeof(M_DATA); + strcpy(data->text, text); + UI_AddChild(node); +} + +void UI_Label_Measure( + const char *const text, float *const out_w, float *const out_h) +{ + UI_Label_MeasureEx(text, out_w, out_h, m_DefaultSettings); +} + +void UI_Label_MeasureEx( + const char *const text, float *const out_w, float *const out_h, + const UI_LABEL_SETTINGS settings) +{ + TEXTSTRING *const textstring = M_CreateText(0, 0, text, settings); + if (out_w != nullptr) { + *out_w = Text_GetWidth(textstring) * g_Config.ui.text_scale; + } + if (out_h != nullptr) { + *out_h = Text_GetHeight(textstring) * g_Config.ui.text_scale; + } + Text_Remove(textstring); +} diff --git a/src/libtrx/game/ui/elements/modal.c b/src/libtrx/game/ui/elements/modal.c new file mode 100644 index 000000000..9aec8632e --- /dev/null +++ b/src/libtrx/game/ui/elements/modal.c @@ -0,0 +1,33 @@ +#include "game/ui/elements/modal.h" + +#include "game/text.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/helpers.h" + +static void M_Measure(UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = UI_LayoutWrapper, + .draw = UI_DrawWrapper, +}; + +static void M_Measure(UI_NODE *const node) +{ + node->measure_w = UI_GetCanvasWidth(); + node->measure_h = UI_GetCanvasHeight() - TEXT_HEIGHT_FIXED; +} + +void UI_BeginModal(const float x, const float y) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, 0); + UI_AddChild(node); + UI_PushCurrent(node); + UI_BeginAnchor(x, y); +} + +void UI_EndModal(void) +{ + UI_EndAnchor(); + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/offset.c b/src/libtrx/game/ui/elements/offset.c new file mode 100644 index 000000000..80275e1be --- /dev/null +++ b/src/libtrx/game/ui/elements/offset.c @@ -0,0 +1,13 @@ +#include "game/ui/elements/offset.h" + +#include "game/ui/elements/pad.h" + +void UI_BeginOffset(const float x, const float y) +{ + UI_BeginPadEx(x, -x, y, -y); +} + +void UI_EndOffset(void) +{ + UI_EndPad(); +} diff --git a/src/libtrx/game/ui/elements/pad.c b/src/libtrx/game/ui/elements/pad.c new file mode 100644 index 000000000..eec083e22 --- /dev/null +++ b/src/libtrx/game/ui/elements/pad.c @@ -0,0 +1,65 @@ +#include "game/ui/elements/pad.h" + +#include "config.h" +#include "game/ui/helpers.h" + +typedef struct { + float t; + float r; + float d; + float l; +} M_DATA; + +static void M_Measure(UI_NODE *node); +static void M_Layout(UI_NODE *node, float x, float y, float w, float h); + +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = M_Layout, + .draw = UI_DrawWrapper, +}; + +static void M_Measure(UI_NODE *const node) +{ + UI_MeasureWrapper(node); + const M_DATA *const data = node->data; + node->measure_w += data->l + data->r; + node->measure_h += data->t + data->d; +} + +static void M_Layout( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + UI_LayoutBasic(node, x, y, w, h); + const M_DATA *const data = node->data; + UI_NODE *child = node->first_child; + while (child != nullptr) { + child->ops->layout( + child, x + data->l, y + data->t, w - data->l - data->r, + h - data->t - data->d); + child = child->next_sibling; + } +} + +void UI_BeginPad(const float x, const float y) +{ + UI_BeginPadEx(x, x, y, y); +} + +void UI_BeginPadEx(const float l, const float r, const float t, const float d) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + data->t = t * g_Config.ui.text_scale; + data->r = r * g_Config.ui.text_scale; + data->d = d * g_Config.ui.text_scale; + data->l = l * g_Config.ui.text_scale; + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndPad(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/prompt.c b/src/libtrx/game/ui/elements/prompt.c new file mode 100644 index 000000000..6a79cee6a --- /dev/null +++ b/src/libtrx/game/ui/elements/prompt.c @@ -0,0 +1,253 @@ +#include "game/ui/elements/prompt.h" + +#include "game/const.h" +#include "game/input.h" +#include "game/ui/common.h" +#include "game/ui/elements/flash.h" +#include "game/ui/elements/label.h" +#include "game/ui/events.h" +#include "game/ui/helpers.h" +#include "log.h" +#include "memory.h" +#include "strings.h" +#include "utils.h" + +#include + +typedef struct { + UI_PROMPT_STATE *state; +} M_DATA; + +static const char m_ValidPromptChars[] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-: "; + +static void M_Layout(UI_NODE *node, float x, float y, float w, float h); + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = M_Layout, + .draw = UI_DrawWrapper, +}; + +static void M_MoveCaretLeft(UI_PROMPT_STATE *s); +static void M_MoveCaretRight(UI_PROMPT_STATE *s); +static void M_MoveCaretStart(UI_PROMPT_STATE *s); +static void M_MoveCaretEnd(UI_PROMPT_STATE *s); +static void M_DeleteCharBack(UI_PROMPT_STATE *s); +static void M_Confirm(UI_PROMPT_STATE *s); +static void M_Cancel(UI_PROMPT_STATE *s); +static void M_Clear(UI_PROMPT_STATE *s); + +static void M_HandleKeyDown(const EVENT *event, void *user_data); +static void M_HandleTextEdit(const EVENT *event, void *user_data); + +static void M_Layout( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + UI_LayoutBasic(node, x, y, w, h); + const M_DATA *const data = node->data; + const UI_PROMPT_STATE *const s = data->state; + UI_NODE *const prompt = node->first_child; + UI_NODE *const caret = prompt->next_sibling; + prompt->ops->layout(prompt, x, y, w, h); + + const char old = s->current_text[s->caret_pos]; + s->current_text[s->caret_pos] = '\0'; + float caret_pos; + UI_Label_Measure(s->current_text, &caret_pos, nullptr); + s->current_text[s->caret_pos] = old; + + caret->ops->layout(caret, x + caret_pos, y, w, h); +} + +static void M_MoveCaretLeft(UI_PROMPT_STATE *const s) +{ + if (s->caret_pos > 0) { + s->caret_pos--; + } +} + +static void M_MoveCaretRight(UI_PROMPT_STATE *const s) +{ + if (s->caret_pos < (int32_t)strlen(s->current_text)) { + s->caret_pos++; + } +} + +static void M_MoveCaretStart(UI_PROMPT_STATE *const s) +{ + s->caret_pos = 0; +} + +static void M_MoveCaretEnd(UI_PROMPT_STATE *const s) +{ + s->caret_pos = strlen(s->current_text); +} + +static void M_DeleteCharBack(UI_PROMPT_STATE *const s) +{ + if (s->caret_pos <= 0) { + return; + } + + memmove( + s->current_text + s->caret_pos - 1, s->current_text + s->caret_pos, + strlen(s->current_text) + 1 - s->caret_pos); + s->caret_pos--; +} + +static void M_Confirm(UI_PROMPT_STATE *const s) +{ + if (String_IsEmpty(s->current_text)) { + M_Cancel(s); + return; + } + UI_FireEvent((EVENT) { + .name = "confirm", + .sender = s, + .data = s->current_text, + }); + M_Clear(s); +} + +static void M_Cancel(UI_PROMPT_STATE *const s) +{ + UI_FireEvent((EVENT) { + .name = "cancel", + .sender = s, + .data = s->current_text, + }); + M_Clear(s); +} + +static void M_Clear(UI_PROMPT_STATE *const s) +{ + strcpy(s->current_text, ""); + s->caret_pos = 0; +} + +static void M_HandleKeyDown(const EVENT *const event, void *const user_data) +{ + const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; + UI_PROMPT_STATE *const s = user_data; + + if (!s->is_focused) { + return; + } + + // clang-format off + switch (key) { + case UI_KEY_LEFT: M_MoveCaretLeft(s); break; + case UI_KEY_RIGHT: M_MoveCaretRight(s); break; + case UI_KEY_HOME: M_MoveCaretStart(s); break; + case UI_KEY_END: M_MoveCaretEnd(s); break; + case UI_KEY_BACK: M_DeleteCharBack(s); break; + case UI_KEY_RETURN: M_Confirm(s); break; + case UI_KEY_ESCAPE: M_Cancel(s); break; + default: break; + } + // clang-format on +} + +static void M_HandleTextEdit(const EVENT *const event, void *const user_data) +{ + const char *insert_string = event->data; + const size_t insert_length = strlen(insert_string); + UI_PROMPT_STATE *const s = user_data; + + if (!s->is_focused) { + return; + } + + if (strlen(insert_string) != 1 + || strstr(m_ValidPromptChars, insert_string) == nullptr) { + return; + } + + const size_t available_space = + s->current_text_capacity - strlen(s->current_text); + if (insert_length >= available_space) { + s->current_text_capacity *= 2; + s->current_text = + Memory_Realloc(s->current_text, s->current_text_capacity); + } + + memmove( + s->current_text + s->caret_pos + insert_length, + s->current_text + s->caret_pos, + strlen(s->current_text) + 1 - s->caret_pos); + memcpy(s->current_text + s->caret_pos, insert_string, insert_length); + + s->caret_pos += insert_length; +} + +void UI_Prompt_Init(UI_PROMPT_STATE *const s) +{ + s->is_focused = false; + s->current_text_capacity = 30; + s->current_text = Memory_Alloc(s->current_text_capacity); + s->listener1 = UI_Subscribe("key_down", nullptr, M_HandleKeyDown, s); + s->listener2 = UI_Subscribe("text_edit", nullptr, M_HandleTextEdit, s); + UI_Flash_Init(&s->flash, LOGIC_FPS * 2 / 3); +} + +void UI_Prompt_Free(UI_PROMPT_STATE *const s) +{ + UI_Unsubscribe(s->listener1); + UI_Unsubscribe(s->listener2); + UI_Flash_Free(&s->flash); + Memory_FreePointer(&s->current_text); +} + +void UI_Prompt_Control(UI_PROMPT_STATE *const s) +{ + UI_Flash_Control(&s->flash); +} + +void UI_Prompt(UI_PROMPT_STATE *const s) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + data->state = s; + UI_AddChild(node); + UI_PushCurrent(node); + UI_LabelEx( + s->current_text != nullptr ? s->current_text : "", + (UI_LABEL_SETTINGS) { .scale = 1.0f, .z = 16 }); + if (s->is_focused) { + UI_BeginFlash(&s->flash); + } + UI_LabelEx( + "\\{button left}", (UI_LABEL_SETTINGS) { .scale = 1.0f, .z = 8 }); + if (s->is_focused) { + UI_EndFlash(); + } + UI_PopCurrent(); +} + +void UI_Prompt_SetFocus(UI_PROMPT_STATE *const s, const bool is_focused) +{ + if (s->is_focused == is_focused) { + return; + } + s->is_focused = is_focused; + s->flash.count = 0; + if (is_focused) { + Input_EnterListenMode(); + } else { + Input_ExitListenMode(); + } +} + +void UI_Prompt_Clear(UI_PROMPT_STATE *const s) +{ + M_Clear(s); +} + +void UI_Prompt_ChangeText(UI_PROMPT_STATE *const s, const char *const new_text) +{ + Memory_FreePointer(&s->current_text); + s->current_text = Memory_DupStr(new_text); + s->caret_pos = strlen(new_text); +} diff --git a/src/libtrx/game/ui/elements/requester.c b/src/libtrx/game/ui/elements/requester.c new file mode 100644 index 000000000..aced27ddc --- /dev/null +++ b/src/libtrx/game/ui/elements/requester.c @@ -0,0 +1,216 @@ +#include "game/ui/elements/requester.h" + +#include "config.h" +#include "game/input.h" +#include "game/text.h" +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/fixed.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/hide.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/offset.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/resize.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "game/ui/elements/window.h" +#include "utils.h" + +static void M_UpArrow(const UI_REQUESTER_STATE *s); +static void M_DownArrow(const UI_REQUESTER_STATE *s); + +static void M_UpArrow(const UI_REQUESTER_STATE *const s) +{ + UI_BeginHide(s->vis_row == 0); + UI_Spacer(0.0f, 4.0f); + UI_BeginAnchor(0.5f, 0.5f); + UI_BeginFixed(0.5f, 1.5f); + UI_LabelEx("\\{arrow up}", (UI_LABEL_SETTINGS) { .scale = 0.7 }); + UI_EndFixed(); + UI_EndAnchor(); + UI_EndHide(); +} + +static void M_DownArrow(const UI_REQUESTER_STATE *const s) +{ + UI_BeginHide(s->vis_row + s->vis_rows >= s->max_rows); + UI_BeginAnchor(0.5f, 0.5f); + UI_BeginFixed(0.5f, 0.0f); + UI_LabelEx("\\{arrow down}", (UI_LABEL_SETTINGS) { .scale = 0.7 }); + UI_EndFixed(); + UI_EndAnchor(); + UI_EndHide(); + UI_Spacer(0.0f, 4.0f); +} + +void UI_Requester_Init( + UI_REQUESTER_STATE *const s, const int32_t vis_rows, const int32_t max_rows, + const bool is_selectable) +{ + s->vis_row = 0; + s->sel_row = 0; + s->vis_rows = vis_rows; + s->max_rows = max_rows; + s->is_selectable = is_selectable; + s->row_pad = 20.0f; + s->row_spacing = 3.0f; + s->show_arrows = false; + s->reserve_space = false; +} + +void UI_Requester_Free(UI_REQUESTER_STATE *const s) +{ +} + +int32_t UI_Requester_Control(UI_REQUESTER_STATE *const s) +{ + if (s->is_selectable) { + if (g_InputDB.menu_down) { + if (s->sel_row + 1 < s->max_rows) { + s->sel_row++; + } else if (g_Config.ui.enable_wraparound) { + s->sel_row = 0; + } + } else if (g_InputDB.menu_up) { + if (s->sel_row > 0) { + s->sel_row--; + } else if (g_Config.ui.enable_wraparound) { + s->sel_row = s->max_rows - 1; + } + } + CLAMP(s->vis_row, s->sel_row - s->vis_rows + 1, s->sel_row); + } else { + if (g_InputDB.menu_down) { + if (s->vis_row + 1 <= s->max_rows - s->vis_rows) { + s->vis_row++; + } else if (g_Config.ui.enable_wraparound) { + s->vis_row = 0; + } + } else if (g_InputDB.menu_up) { + if (s->vis_row > 0) { + s->vis_row--; + } else if (g_Config.ui.enable_wraparound) { + s->vis_row = s->max_rows - 1; + } + } + } + + CLAMPG(s->vis_row, s->max_rows - s->vis_rows); + CLAMPL(s->vis_row, 0); + + if (s->is_selectable) { + if (g_InputDB.menu_back) { + return UI_REQUESTER_CANCEL; + } + if (g_InputDB.menu_confirm) { + return s->sel_row; + } + } + return UI_REQUESTER_NO_CHOICE; +} + +void UI_Requester_SetMaxRows(UI_REQUESTER_STATE *const s, const size_t max_rows) +{ + s->max_rows = max_rows; +} + +void UI_Requester_SetVisibleRows( + UI_REQUESTER_STATE *const s, const size_t visible_rows) +{ + s->vis_rows = visible_rows; + CLAMPL(s->vis_rows, 0); + if (s->sel_row != -1) { + CLAMP(s->vis_row, s->sel_row - s->vis_rows + 1, s->sel_row); + } + CLAMP(s->vis_row, 0, s->max_rows - s->vis_rows); +} + +int32_t UI_Requester_GetFirstRow(const UI_REQUESTER_STATE *const s) +{ + return s->vis_row; +} + +int32_t UI_Requester_GetLastRow(const UI_REQUESTER_STATE *const s) +{ + return MIN(s->vis_row + s->vis_rows, s->max_rows); +} + +int32_t UI_Requester_GetCurrentRow(const UI_REQUESTER_STATE *s) +{ + return s->sel_row; +} + +bool UI_Requester_IsRowVisible( + const UI_REQUESTER_STATE *const s, const int32_t i) +{ + return i >= UI_Requester_GetFirstRow(s) && i < UI_Requester_GetLastRow(s); +} + +bool UI_Requester_IsRowSelected( + const UI_REQUESTER_STATE *const s, const int32_t i) +{ + return i == s->sel_row; +} + +void UI_BeginRequester( + const UI_REQUESTER_STATE *const s, const char *const title) +{ + UI_BeginWindow(); + UI_WindowTitle(title); + UI_BeginWindowBody(); + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + }); + + if (s->show_arrows) { + M_UpArrow(s); + } + if (s->reserve_space) { + UI_BeginResize( + -1.0f, + s->vis_rows * TEXT_HEIGHT_FIXED + + (s->vis_rows - 1) * s->row_spacing); + } + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + .spacing = { .v = s->row_spacing }, + }); +} + +void UI_EndRequester(const UI_REQUESTER_STATE *const s) +{ + UI_EndStack(); + + if (s->reserve_space) { + UI_EndResize(); + } + if (s->show_arrows) { + M_DownArrow(s); + } + + UI_EndStack(); + UI_EndWindowBody(); + UI_EndWindow(); +} + +void UI_BeginRequesterRow(const UI_REQUESTER_STATE *const s, const int32_t i) +{ + UI_BeginPad(0.0f, TR_VERSION == 1 ? -1.0f : 0.0f); + if (UI_Requester_IsRowSelected(s, i)) { + UI_BeginFrame(UI_FRAME_SELECTED_OPTION); + } + UI_BeginPad(s->row_pad, TR_VERSION == 1 ? 1.0f : 0.0f); +} + +void UI_EndRequesterRow(const UI_REQUESTER_STATE *const s, const int32_t i) +{ + UI_EndPad(); + if (UI_Requester_IsRowSelected(s, i)) { + UI_EndFrame(); + } + UI_EndPad(); +} diff --git a/src/libtrx/game/ui/elements/resize.c b/src/libtrx/game/ui/elements/resize.c new file mode 100644 index 000000000..3a43dc3cb --- /dev/null +++ b/src/libtrx/game/ui/elements/resize.c @@ -0,0 +1,53 @@ +#include "game/ui/elements/resize.h" + +#include "config.h" +#include "game/ui/helpers.h" + +typedef struct { + float x; + float y; +} M_DATA; + +static void M_Measure(UI_NODE *node); +static void M_Draw(const UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = UI_LayoutWrapper, + .draw = M_Draw, +}; + +static void M_Measure(UI_NODE *const node) +{ + UI_MeasureWrapper(node); + const M_DATA *const data = node->data; + if (data->x >= 0.0f) { + node->measure_w = data->x; + } + if (data->y >= 0.0f) { + node->measure_h = data->y; + } +} + +void M_Draw(const UI_NODE *const node) +{ + if (node->measure_w <= 0.0f || node->measure_h <= 0.0f) { + return; + } + UI_DrawWrapper(node); +} + +void UI_BeginResize(const float x, const float y) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + M_DATA *const data = node->data; + data->x = x * g_Config.ui.text_scale; + data->y = y * g_Config.ui.text_scale; + UI_AddChild(node); + UI_PushCurrent(node); +} + +void UI_EndResize(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/spacer.c b/src/libtrx/game/ui/elements/spacer.c new file mode 100644 index 000000000..dd4160e14 --- /dev/null +++ b/src/libtrx/game/ui/elements/spacer.c @@ -0,0 +1,25 @@ +#include "game/ui/elements/spacer.h" + +#include "config.h" +#include "game/ui/helpers.h" + +static void M_Measure(UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = UI_LayoutBasic, + .draw = UI_DrawWrapper, +}; + +static void M_Measure(UI_NODE *const node) +{ + // already done in the constructor +} + +void UI_Spacer(const float w, const float h) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, 0); + node->measure_w = w * g_Config.ui.text_scale; + node->measure_h = h * g_Config.ui.text_scale; + UI_AddChild(node); +} diff --git a/src/libtrx/game/ui/elements/stack.c b/src/libtrx/game/ui/elements/stack.c new file mode 100644 index 000000000..d8a460a33 --- /dev/null +++ b/src/libtrx/game/ui/elements/stack.c @@ -0,0 +1,246 @@ +#include "game/ui/elements/stack.h" + +#include "config.h" +#include "debug.h" +#include "game/ui/helpers.h" +#include "utils.h" + +#include +#include + +typedef struct { + UI_STACK_SETTINGS settings; +} M_DATA; + +static float M_CalcChildW(const UI_NODE *node, const UI_NODE *child); +static float M_CalcChildH(const UI_NODE *node, const UI_NODE *child); +static float M_CalcStartX(const UI_NODE *node, const UI_NODE *child); +static float M_CalcStartY(const UI_NODE *node, const UI_NODE *child); +static void M_Measure(UI_NODE *node); +static void M_Layout(UI_NODE *node, float x, float y, float w, float h); + +static const UI_WIDGET_OPS m_Ops = { + .measure = M_Measure, + .layout = M_Layout, + .draw = UI_DrawWrapper, +}; + +static float M_CalcChildW(const UI_NODE *const node, const UI_NODE *const child) +{ + M_DATA *const data = node->data; + if (data->settings.align.h == UI_STACK_H_ALIGN_SPAN) { + return MAX(child->measure_w, node->w); + } + return child->measure_w; +} + +static float M_CalcChildH(const UI_NODE *const node, const UI_NODE *const child) +{ + M_DATA *const data = node->data; + if (data->settings.align.v == UI_STACK_V_ALIGN_SPAN) { + return MAX(child->measure_h, node->h); + } + return child->measure_h; +} + +static float M_CalcStartX(const UI_NODE *const node, const UI_NODE *const child) +{ + M_DATA *const data = node->data; + switch (data->settings.align.h) { + case UI_STACK_H_ALIGN_SPAN: + case UI_STACK_H_ALIGN_LEFT: + return node->x; + case UI_STACK_H_ALIGN_CENTER: + return node->x + (node->w - child->measure_w) * 0.5f; + case UI_STACK_H_ALIGN_RIGHT: + return node->x + node->w - child->measure_w; + case UI_STACK_H_ALIGN_DISTRIBUTE: + ASSERT_FAIL(); + } + return 0.0f; +} + +static float M_CalcStartY(const UI_NODE *const node, const UI_NODE *const child) +{ + M_DATA *const data = node->data; + switch (data->settings.align.v) { + case UI_STACK_V_ALIGN_SPAN: + case UI_STACK_V_ALIGN_TOP: + return node->y; + case UI_STACK_V_ALIGN_CENTER: + return node->y + (node->h - child->measure_h) * 0.5f; + case UI_STACK_V_ALIGN_BOTTOM: + return node->y + node->h - child->measure_h; + case UI_STACK_V_ALIGN_DISTRIBUTE: + ASSERT_FAIL(); + } + return 0.0f; +} + +static void M_Measure(UI_NODE *const node) +{ + node->measure_w = 0.0f; + node->measure_h = 0.0f; + UI_NODE *child = node->first_child; + M_DATA *const data = node->data; + const float scale = g_Config.ui.text_scale; + while (child != nullptr) { + if (data->settings.orientation == UI_STACK_VERTICAL) { + node->measure_w = MAX(node->measure_w, child->measure_w); + node->measure_h += child->measure_h; + if (child->next_sibling != nullptr) { + node->measure_h += data->settings.spacing.v * scale; + } + } else { + node->measure_h = MAX(node->measure_h, child->measure_h); + node->measure_w += child->measure_w; + if (child->next_sibling != nullptr) { + node->measure_w += data->settings.spacing.h * scale; + } + } + child = child->next_sibling; + } +} + +static void M_Layout( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + UI_LayoutBasic(node, x, y, w, h); + M_DATA *const data = node->data; + + // Count children and compute the total size they occupy on the main axis + // including the base spacing from the settings. + int32_t child_count = 0; + float total_child_main_size = 0.0f; + UI_NODE *child = node->first_child; + while (child != NULL) { + switch (data->settings.orientation) { + case UI_STACK_HORIZONTAL: + total_child_main_size += child->measure_w; + break; + case UI_STACK_VERTICAL: + total_child_main_size += child->measure_h; + break; + } + child_count++; + child = child->next_sibling; + } + + // If there is at least one gap between children, compute the normal + // (configured) total spacing on the main axis. If only 1 child or 0 + // children, there's no gap to distribute leftover space into. + const int32_t gaps = (child_count > 1) ? (child_count - 1) : 0; + const float scale = g_Config.ui.text_scale; + float base_spacing = 0.0f; + switch (data->settings.orientation) { + case UI_STACK_HORIZONTAL: + base_spacing = data->settings.spacing.h * gaps * scale; + break; + case UI_STACK_VERTICAL: + base_spacing = data->settings.spacing.v * gaps * scale; + break; + } + + // The space that the children + base spacing absolutely need + const float needed_size = total_child_main_size + base_spacing; + + // The leftover that we can distribute among the (child_count - 1) internal + // gaps. + float leftover = 0.0f; + float extra_per_gap = 0.0f; + switch (data->settings.orientation) { + case UI_STACK_HORIZONTAL: + leftover = w - needed_size; + break; + case UI_STACK_VERTICAL: + leftover = h - needed_size; + break; + } + + if ((data->settings.orientation == UI_STACK_HORIZONTAL + && data->settings.align.h == UI_STACK_H_ALIGN_DISTRIBUTE) + || (data->settings.orientation == UI_STACK_VERTICAL + && data->settings.align.v == UI_STACK_V_ALIGN_DISTRIBUTE)) { + if (gaps > 0 && leftover > 0.0f) { + extra_per_gap = leftover / (float)gaps; + } + } + + // Now we actually lay out the children + float cx = x; + float cy = y; + child = node->first_child; + while (child != nullptr) { + const float cw = M_CalcChildW(node, child); + const float ch = M_CalcChildH(node, child); + + switch (data->settings.orientation) { + case UI_STACK_HORIZONTAL: + // For horizontal: vertical alignment is determined by M_CalcStartY + cy = M_CalcStartY(node, child); + + // Lay out the child + child->ops->layout(child, cx, cy, cw, ch); + + // Advance cx for the next child + cx += cw; + // Add normal spacing + any extra leftover that we are distributing + if (child->next_sibling != nullptr) { + cx += data->settings.spacing.h * scale + extra_per_gap; + } + break; + + case UI_STACK_VERTICAL: + cx = M_CalcStartX(node, child); + + child->ops->layout(child, cx, cy, cw, ch); + + cy += ch; + if (child->next_sibling != nullptr) { + cy += data->settings.spacing.v * scale + extra_per_gap; + } + break; + } + + child = child->next_sibling; + } +} + +UI_NODE *UI_CreateStack(const UI_STACK_SETTINGS settings) +{ + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(M_DATA)); + if (node == nullptr) { + return nullptr; + } + M_DATA *const data = node->data; + data->settings = settings; + return node; +} + +void UI_BeginStack(const UI_STACK_ORIENTATION orientation) +{ + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = orientation, + .align = { + .h = UI_STACK_H_ALIGN_LEFT, + .v = UI_STACK_V_ALIGN_TOP, + }, + .spacing = { + .h = 0.0f, + .v = 0.0f, + }, + }); +} + +void UI_BeginStackEx(const UI_STACK_SETTINGS settings) +{ + UI_NODE *const child = UI_CreateStack(settings); + UI_AddChild(child); + UI_PushCurrent(child); +} + +void UI_EndStack(void) +{ + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/elements/window.c b/src/libtrx/game/ui/elements/window.c new file mode 100644 index 000000000..985d60b61 --- /dev/null +++ b/src/libtrx/game/ui/elements/window.c @@ -0,0 +1,50 @@ +#include "game/ui/elements/window.h" + +#include "game/ui/elements/anchor.h" +#include "game/ui/elements/frame.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/stack.h" + +void UI_BeginWindow(void) +{ + const float outer_pad = 2.0f; + const float title_spacing = 3.0f; + UI_BeginFrame(UI_FRAME_DIALOG_BACKGROUND); + UI_BeginPad(outer_pad, outer_pad); + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + .spacing = { .v = title_spacing }, + }); +} + +void UI_EndWindow(void) +{ + UI_EndStack(); + UI_EndPad(); + UI_EndFrame(); +} + +void UI_BeginWindowBody(void) +{ + const float body_pad = TR_VERSION == 2 ? 4.0f : 8.0f; + UI_BeginPad(body_pad, body_pad); +} + +void UI_EndWindowBody(void) +{ + UI_EndPad(); +} + +void UI_WindowTitle(const char *const title) +{ + UI_BeginFrame(UI_FRAME_DIALOG_HEADING); + UI_BeginPad(10.0f, TR_VERSION == 2 ? 1.0f : 2.0f); + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(title); + UI_EndAnchor(); + UI_EndPad(); + UI_EndFrame(); +} diff --git a/src/libtrx/game/ui/events.c b/src/libtrx/game/ui/events.c index 13ea6de17..54d87be14 100644 --- a/src/libtrx/game/ui/events.c +++ b/src/libtrx/game/ui/events.c @@ -6,35 +6,19 @@ static EVENT_MANAGER *m_EventManager = nullptr; -static void M_HandleConfigChange(const EVENT *event, void *data); - -static void M_HandleConfigChange(const EVENT *const event, void *const data) -{ - if (m_EventManager != nullptr) { - const EVENT new_event = { - .name = "canvas_resize", - .sender = nullptr, - .data = nullptr, - }; - EventManager_Fire(m_EventManager, &new_event); - UI_HandleLayoutChange(); - } -} - -void UI_Events_Init(void) +void UI_InitEvents(void) { m_EventManager = EventManager_Create(); - Config_SubscribeChanges(M_HandleConfigChange, nullptr); } -void UI_Events_Shutdown(void) +void UI_ShutdownEvents(void) { EventManager_Free(m_EventManager); m_EventManager = nullptr; } -int32_t UI_Events_Subscribe( - const char *const event_name, const UI_WIDGET *const sender, +int32_t UI_Subscribe( + const char *const event_name, const void *const sender, const EVENT_LISTENER listener, void *const user_data) { ASSERT(m_EventManager != nullptr); @@ -42,16 +26,16 @@ int32_t UI_Events_Subscribe( m_EventManager, event_name, sender, listener, user_data); } -void UI_Events_Unsubscribe(const int32_t listener_id) +void UI_Unsubscribe(const int32_t listener_id) { if (m_EventManager != nullptr) { EventManager_Unsubscribe(m_EventManager, listener_id); } } -void UI_Events_Fire(const EVENT *const event) +void UI_FireEvent(const EVENT event) { if (m_EventManager != nullptr) { - EventManager_Fire(m_EventManager, event); + EventManager_Fire(m_EventManager, &event); } } diff --git a/src/libtrx/game/ui/helpers.c b/src/libtrx/game/ui/helpers.c new file mode 100644 index 000000000..e20741d36 --- /dev/null +++ b/src/libtrx/game/ui/helpers.c @@ -0,0 +1,50 @@ +#include "game/ui/helpers.h" + +#include "utils.h" + +void UI_MeasureWrapper(UI_NODE *const node) +{ + node->measure_w = 0.0f; + node->measure_h = 0.0f; + const UI_NODE *child = node->first_child; + while (child != nullptr) { + node->measure_w = MAX(node->measure_w, child->measure_w); + node->measure_h = MAX(node->measure_h, child->measure_h); + child = child->next_sibling; + } +} + +void UI_LayoutBasic( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + node->x = x; + node->y = y; + node->w = w; + node->h = h; +} + +void UI_LayoutWrapper( + UI_NODE *const node, const float x, const float y, const float w, + const float h) +{ + UI_LayoutBasic(node, x, y, w, h); + UI_NODE *child = node->first_child; + while (child != nullptr) { + if (child->ops->layout != nullptr) { + child->ops->layout(child, x, y, w, h); + } + child = child->next_sibling; + } +} + +void UI_DrawWrapper(const UI_NODE *const node) +{ + const UI_NODE *child = node->first_child; + while (child != nullptr) { + if (child->ops->draw != nullptr) { + child->ops->draw(child); + } + child = child->next_sibling; + } +} diff --git a/src/libtrx/game/ui/helpers.h b/src/libtrx/game/ui/helpers.h new file mode 100644 index 000000000..88d8eccc6 --- /dev/null +++ b/src/libtrx/game/ui/helpers.h @@ -0,0 +1,9 @@ +#pragma once + +#include "game/ui/common.h" + +// Repetitive widget ops strategies +void UI_MeasureWrapper(UI_NODE *node); +void UI_LayoutBasic(UI_NODE *node, float x, float y, float w, float h); +void UI_LayoutWrapper(UI_NODE *node, float x, float y, float w, float h); +void UI_DrawWrapper(const UI_NODE *node); diff --git a/src/libtrx/game/ui/hud/console.c b/src/libtrx/game/ui/hud/console.c new file mode 100644 index 000000000..805ba39c6 --- /dev/null +++ b/src/libtrx/game/ui/hud/console.c @@ -0,0 +1,168 @@ +#include "game/ui/hud/console.h" + +#include "game/console.h" +#include "game/text.h" +#include "game/ui/elements/modal.h" +#include "game/ui/elements/pad.h" +#include "game/ui/elements/prompt.h" +#include "game/ui/elements/spacer.h" +#include "game/ui/elements/stack.h" +#include "game/ui/events.h" +#include "game/ui/helpers.h" +#include "game/ui/hud/console_logs.h" +#include "utils.h" + +static void M_Draw(const UI_NODE *node); + +static const UI_WIDGET_OPS m_Ops = { + .measure = UI_MeasureWrapper, + .layout = UI_LayoutWrapper, + .draw = M_Draw, +}; + +static void M_MoveHistoryUp(UI_CONSOLE_STATE *s); +static void M_MoveHistoryDown(UI_CONSOLE_STATE *s); +static void M_HandleOpen(const EVENT *event, void *user_data); +static void M_HandleClose(const EVENT *event, void *user_data); +static void M_HandleCancel(const EVENT *event, void *user_data); +static void M_HandleConfirm(const EVENT *event, void *user_data); + +static void M_MoveHistoryUp(UI_CONSOLE_STATE *const s) +{ + s->history_idx--; + CLAMP(s->history_idx, 0, Console_History_GetLength()); + const char *const new_prompt = Console_History_Get(s->history_idx); + UI_Prompt_ChangeText(&s->prompt, new_prompt == nullptr ? "" : new_prompt); +} + +static void M_MoveHistoryDown(UI_CONSOLE_STATE *const s) +{ + s->history_idx++; + CLAMP(s->history_idx, 0, Console_History_GetLength()); + const char *const new_prompt = Console_History_Get(s->history_idx); + UI_Prompt_ChangeText(&s->prompt, new_prompt == nullptr ? "" : new_prompt); +} + +static void M_HandleKeyDown(const EVENT *const event, void *const user_data) +{ + if (!Console_IsOpened()) { + return; + } + + UI_CONSOLE_STATE *const s = user_data; + const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; + + // clang-format off + switch (key) { + case UI_KEY_UP: M_MoveHistoryUp(s); break; + case UI_KEY_DOWN: M_MoveHistoryDown(s); break; + default: break; + } + // clang-format on +} + +static void M_HandleOpen(const EVENT *event, void *user_data) +{ + UI_CONSOLE_STATE *const s = user_data; + UI_Prompt_SetFocus(&s->prompt, true); + s->history_idx = Console_History_GetLength(); +} + +static void M_HandleClose(const EVENT *event, void *user_data) +{ + UI_CONSOLE_STATE *const s = user_data; + UI_Prompt_SetFocus(&s->prompt, false); + UI_Prompt_Clear(&s->prompt); +} + +static void M_HandleCancel(const EVENT *const event, void *const data) +{ + Console_Close(); +} + +static void M_HandleConfirm(const EVENT *event, void *user_data) +{ + UI_CONSOLE_STATE *const s = user_data; + const char *text = event->data; + Console_History_Append(text); + Console_Eval(text); + Console_Close(); + s->history_idx = Console_History_GetLength(); +} + +static void M_Draw(const UI_NODE *node) +{ + UI_CONSOLE_STATE *const s = *(UI_CONSOLE_STATE **)node->data; + UI_DrawWrapper(node); + if (Console_IsOpened() || s->logs.vis_lines > 0) { + Console_DrawBackdrop(); + } +} + +void UI_Console_Init(UI_CONSOLE_STATE *const s) +{ + UI_Prompt_Init(&s->prompt); + UI_ConsoleLogs_Init(&s->logs); + + struct { + const char *event_name; + const void *sender; + EVENT_LISTENER handler; + } listeners[] = { + { "console_open", nullptr, M_HandleOpen }, + { "console_close", nullptr, M_HandleClose }, + { "cancel", &s->prompt, M_HandleCancel }, + { "confirm", &s->prompt, M_HandleConfirm }, + { "key_down", nullptr, M_HandleKeyDown }, + { 0 }, + }; + for (int32_t i = 0; listeners[i].event_name != nullptr; i++) { + s->listeners[i] = UI_Subscribe( + listeners[i].event_name, listeners[i].sender, listeners[i].handler, + s); + } + + s->history_idx = -1; +} + +void UI_Console_Free(UI_CONSOLE_STATE *const s) +{ + UI_ConsoleLogs_Free(&s->logs); + UI_Prompt_Free(&s->prompt); + for (int32_t i = 0; i < 5; i++) { + UI_Unsubscribe(s->listeners[i]); + } +} + +void UI_Console_Control(UI_CONSOLE_STATE *const s) +{ + UI_Prompt_Control(&s->prompt); +} + +void UI_Console(UI_CONSOLE_STATE *const s) +{ + UI_Prompt_SetFocus(&s->prompt, Console_IsOpened()); + + UI_NODE *const node = UI_AllocNode(&m_Ops, sizeof(UI_CONSOLE_STATE *)); + *(UI_CONSOLE_STATE **)node->data = s; + UI_AddChild(node); + UI_PushCurrent(node); + + UI_BeginModal(0.0f, 1.0f); + UI_BeginPad(5.0f, 5.0f); + UI_BeginStack(UI_STACK_VERTICAL); + + UI_ConsoleLogs(&s->logs); + UI_Spacer(0.0f, 8.0f); + if (Console_IsOpened()) { + UI_Prompt(&s->prompt); + } else { + UI_Spacer(0.0f, TEXT_HEIGHT_FIXED); + } + + UI_EndStack(); + UI_EndModal(); + UI_EndPad(); + + UI_PopCurrent(); +} diff --git a/src/libtrx/game/ui/hud/console_logs.c b/src/libtrx/game/ui/hud/console_logs.c new file mode 100644 index 000000000..278ca3659 --- /dev/null +++ b/src/libtrx/game/ui/hud/console_logs.c @@ -0,0 +1,100 @@ +#include "game/ui/hud/console_logs.h" + +#include "debug.h" +#include "game/clock.h" +#include "game/text.h" +#include "game/ui/elements/label.h" +#include "game/ui/elements/stack.h" +#include "game/ui/events.h" +#include "memory.h" +#include "strings.h" + +#include + +#define M_LOG_SCALE 0.8f +#define M_MAX_LOG_LINES 20 +#define M_DELAY_PER_CHAR 0.2 + +static void M_ScrollLogs(UI_CONSOLE_LOGS *s); +static void M_UpdateLogCount(UI_CONSOLE_LOGS *s); +static void M_HandleLog(const EVENT *event, void *user_data); + +static void M_ScrollLogs(UI_CONSOLE_LOGS *const s) +{ + int32_t i = s->max_lines - 1; + while (i >= 0 && !s->logs[i].expire_at) { + i--; + } + + bool need_layout = false; + while (i >= 0 && s->logs[i].expire_at + && Clock_GetRealTime() >= s->logs[i].expire_at) { + s->logs[i].expire_at = 0.0; + Memory_FreePointer(&s->logs[i].text); + need_layout = true; + i--; + } + + if (need_layout) { + M_UpdateLogCount(s); + } +} +static void M_UpdateLogCount(UI_CONSOLE_LOGS *const s) +{ + s->vis_lines = 0; + for (int32_t i = s->max_lines - 1; i >= 0; i--) { + if (s->logs[i].expire_at != 0.0) { + s->vis_lines = i + 1; + break; + } + } +} + +static void M_HandleLog(const EVENT *const event, void *const user_data) +{ + const char *text = event->data; + UI_CONSOLE_LOGS *const s = user_data; + Memory_FreePointer(&s->logs[s->max_lines - 1].text); + for (int32_t i = s->max_lines - 1; i > 0; i--) { + s->logs[i] = s->logs[i - 1]; + } + + s->logs[0].expire_at = + Clock_GetRealTime() + strlen(text) * M_DELAY_PER_CHAR; + s->logs[0].text = String_WordWrap(text, Text_GetMaxLineLength()); + M_UpdateLogCount(s); +} + +void UI_ConsoleLogs_Init(UI_CONSOLE_LOGS *const s) +{ + if (s->max_lines <= 0) { + s->max_lines = M_MAX_LOG_LINES; + } + s->logs = Memory_Alloc(s->max_lines * sizeof(UI_CONSOLE_LOG_LINE)); + s->vis_lines = 0; + s->listener_id = UI_Subscribe("console_log", nullptr, M_HandleLog, s); +} + +void UI_ConsoleLogs_Free(UI_CONSOLE_LOGS *const s) +{ + Memory_FreePointer(&s->logs); + UI_Unsubscribe(s->listener_id); +} + +void UI_ConsoleLogs(UI_CONSOLE_LOGS *const s) +{ + ASSERT(s != nullptr); + M_ScrollLogs(s); + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .align = { + .h = UI_STACK_H_ALIGN_LEFT, + .v = UI_STACK_V_ALIGN_CENTER, + }, + }); + for (int32_t i = s->vis_lines - 1; i >= 0; i--) { + UI_LabelEx( + s->logs[i].text, (UI_LABEL_SETTINGS) { .scale = M_LOG_SCALE }); + } + UI_EndStack(); +} diff --git a/src/libtrx/game/ui/widgets/console.c b/src/libtrx/game/ui/widgets/console.c deleted file mode 100644 index e69153dc1..000000000 --- a/src/libtrx/game/ui/widgets/console.c +++ /dev/null @@ -1,310 +0,0 @@ -#include "game/ui/widgets/console.h" - -#include "game/clock.h" -#include "game/console/common.h" -#include "game/console/history.h" -#include "game/text.h" -#include "game/ui/common.h" -#include "game/ui/events.h" -#include "game/ui/widgets/label.h" -#include "game/ui/widgets/prompt.h" -#include "game/ui/widgets/spacer.h" -#include "game/ui/widgets/stack.h" -#include "memory.h" -#include "strings.h" -#include "utils.h" - -#include - -#define WINDOW_MARGIN 5 -#define MAX_LOG_LINES 20 -#define MAX_LISTENERS 4 -#define LOG_MARGIN 10 -#define LOG_SCALE 0.8 -#define DELAY_PER_CHAR 0.2 - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *container; - UI_WIDGET *prompt; - UI_WIDGET *spacer; - char *log_lines; - int32_t logs_on_screen; - int32_t history_idx; - - int32_t listeners[MAX_LISTENERS]; - struct { - double expire_at; - UI_WIDGET *label; - } logs[MAX_LOG_LINES]; -} UI_CONSOLE; - -static void M_MoveHistoryUp(UI_CONSOLE *self); -static void M_MoveHistoryDown(UI_CONSOLE *self); -static void M_DoLayout(UI_CONSOLE *self); -static void M_HandlePromptCancel(const EVENT *event, void *data); -static void M_HandlePromptConfirm(const EVENT *event, void *data); -static void M_HandleCanvasResize(const EVENT *event, void *data); -static void M_UpdateLogCount(UI_CONSOLE *self); - -static int32_t M_GetWidth(const UI_CONSOLE *self); -static int32_t M_GetHeight(const UI_CONSOLE *self); -static void M_SetPosition(UI_CONSOLE *self, int32_t x, int32_t y); -static void M_Control(UI_CONSOLE *self); -static void M_Draw(UI_CONSOLE *self); -static void M_Free(UI_CONSOLE *self); - -static void M_MoveHistoryUp(UI_CONSOLE *const self) -{ - self->history_idx--; - CLAMP(self->history_idx, 0, Console_History_GetLength()); - const char *const new_prompt = Console_History_Get(self->history_idx); - if (new_prompt == nullptr) { - UI_Prompt_ChangeText(self->prompt, ""); - } else { - UI_Prompt_ChangeText(self->prompt, new_prompt); - } -} - -static void M_MoveHistoryDown(UI_CONSOLE *const self) -{ - self->history_idx++; - CLAMP(self->history_idx, 0, Console_History_GetLength()); - const char *const new_prompt = Console_History_Get(self->history_idx); - if (new_prompt == nullptr) { - UI_Prompt_ChangeText(self->prompt, ""); - } else { - UI_Prompt_ChangeText(self->prompt, new_prompt); - } -} - -static void M_DoLayout(UI_CONSOLE *const self) -{ - UI_Stack_SetSize(self->container, M_GetWidth(self), M_GetHeight(self)); - M_SetPosition(self, WINDOW_MARGIN, WINDOW_MARGIN); -} - -static void M_HandlePromptCancel(const EVENT *const event, void *const data) -{ - Console_Close(); -} - -static void M_HandlePromptConfirm(const EVENT *const event, void *const data) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)data; - const char *text = event->data; - Console_History_Append(text); - Console_Eval(text); - Console_Close(); - - self->history_idx = Console_History_GetLength(); -} - -static void M_HandleCanvasResize(const EVENT *event, void *data) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)data; - M_DoLayout(self); -} - -static void M_HandleKeyDown(const EVENT *const event, void *const user_data) -{ - if (!Console_IsOpened()) { - return; - } - - UI_CONSOLE *const self = user_data; - const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; - - // clang-format off - switch (key) { - case UI_KEY_UP: M_MoveHistoryUp(self); break; - case UI_KEY_DOWN: M_MoveHistoryDown(self); break; - default: break; - } - // clang-format on -} - -static void M_UpdateLogCount(UI_CONSOLE *const self) -{ - self->logs_on_screen = 0; - for (int32_t i = MAX_LOG_LINES - 1; i >= 0; i--) { - if (self->logs[i].expire_at != 0.0) { - self->logs_on_screen = i + 1; - break; - } - } -} - -static int32_t M_GetWidth(const UI_CONSOLE *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return UI_GetCanvasWidth() - 2 * WINDOW_MARGIN; -} - -static int32_t M_GetHeight(const UI_CONSOLE *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return UI_GetCanvasHeight() - 2 * WINDOW_MARGIN; -} - -static void M_SetPosition(UI_CONSOLE *const self, int32_t x, int32_t y) -{ - self->container->set_position(self->container, x, y); -} - -static void M_Control(UI_CONSOLE *const self) -{ - if (self->container->control != nullptr) { - self->container->control(self->container); - } -} - -static void M_Draw(UI_CONSOLE *const self) -{ - if (self->vtable.is_hidden) { - return; - } - - if (self->container->draw != nullptr) { - self->container->draw(self->container); - } -} - -static void M_Free(UI_CONSOLE *const self) -{ - self->spacer->free(self->spacer); - self->prompt->free(self->prompt); - self->container->free(self->container); - for (int32_t i = 0; i < MAX_LOG_LINES; i++) { - self->logs[i].label->free(self->logs[i].label); - } - for (int32_t i = 0; i < MAX_LISTENERS; i++) { - UI_Events_Unsubscribe(self->listeners[i]); - } - Memory_Free(self); -} - -UI_WIDGET *UI_Console_Create(void) -{ - UI_CONSOLE *const self = Memory_Alloc(sizeof(UI_CONSOLE)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->container = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, M_GetWidth(self), M_GetHeight(self)); - UI_Stack_SetVAlign(self->container, UI_STACK_V_ALIGN_BOTTOM); - - for (int32_t i = MAX_LOG_LINES - 1; i >= 0; i--) { - self->logs[i].label = - UI_Label_Create("", UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE); - UI_Label_SetScale(self->logs[i].label, LOG_SCALE); - UI_Stack_AddChild(self->container, self->logs[i].label); - } - - self->spacer = UI_Spacer_Create(LOG_MARGIN, LOG_MARGIN); - UI_Stack_AddChild(self->container, self->spacer); - - self->prompt = UI_Prompt_Create(UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE); - UI_Stack_AddChild(self->container, self->prompt); - - int32_t i = 0; - self->listeners[i++] = UI_Events_Subscribe( - "confirm", self->prompt, M_HandlePromptConfirm, self); - self->listeners[i++] = UI_Events_Subscribe( - "cancel", self->prompt, M_HandlePromptCancel, nullptr); - self->listeners[i++] = UI_Events_Subscribe( - "canvas_resize", nullptr, M_HandleCanvasResize, self); - self->listeners[i++] = - UI_Events_Subscribe("key_down", nullptr, M_HandleKeyDown, self); - - M_DoLayout(self); - return (UI_WIDGET *)self; -} - -void UI_Console_HandleOpen(UI_WIDGET *const widget) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)widget; - UI_Prompt_SetFocus(self->prompt, true); - self->history_idx = Console_History_GetLength(); -} - -void UI_Console_HandleClose(UI_WIDGET *const widget) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)widget; - UI_Prompt_SetFocus(self->prompt, false); - UI_Prompt_Clear(self->prompt); -} - -void UI_Console_HandleLog(UI_WIDGET *const widget, const char *const text) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)widget; - - int32_t dst_idx = -1; - for (int32_t i = MAX_LOG_LINES - 1; i > 0; i--) { - if (self->logs[i].label == nullptr) { - continue; - } - UI_Label_ChangeText( - self->logs[i].label, UI_Label_GetText(self->logs[i - 1].label)); - self->logs[i].expire_at = self->logs[i - 1].expire_at; - } - - if (self->logs[0].label == nullptr) { - return; - } - - self->logs[0].expire_at = - Clock_GetRealTime() + strlen(text) * DELAY_PER_CHAR; - - char *wrapped = String_WordWrap(text, Text_GetMaxLineLength()); - UI_Label_ChangeText(self->logs[0].label, wrapped); - Memory_FreePointer(&wrapped); - - M_DoLayout(self); - M_UpdateLogCount(self); -} - -void UI_Console_ScrollLogs(UI_WIDGET *const widget) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)widget; - - int32_t i = MAX_LOG_LINES - 1; - while (i >= 0 && !self->logs[i].expire_at) { - i--; - } - - bool need_layout = false; - while (i >= 0 && self->logs[i].expire_at - && Clock_GetRealTime() >= self->logs[i].expire_at) { - self->logs[i].expire_at = 0.0; - UI_Label_ChangeText(self->logs[i].label, ""); - need_layout = true; - i--; - } - - if (need_layout) { - M_UpdateLogCount(self); - M_DoLayout(self); - } -} - -int32_t UI_Console_GetVisibleLogCount(UI_WIDGET *const widget) -{ - UI_CONSOLE *const self = (UI_CONSOLE *)widget; - return self->logs_on_screen; -} - -int32_t UI_Console_GetMaxLogCount(UI_WIDGET *const widget) -{ - return MAX_LOG_LINES; -} diff --git a/src/libtrx/game/ui/widgets/frame.c b/src/libtrx/game/ui/widgets/frame.c deleted file mode 100644 index c8bc2f366..000000000 --- a/src/libtrx/game/ui/widgets/frame.c +++ /dev/null @@ -1,99 +0,0 @@ -#include "game/text.h" -#include "game/ui/widgets/spacer.h" -#include "memory.h" - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *root; - int32_t margin; - int32_t padding; - bool is_frame_visible; - TEXTSTRING *frame; -} UI_FRAME; - -static int32_t M_GetWidth(const UI_FRAME *self); -static int32_t M_GetHeight(const UI_FRAME *self); -static void M_SetPosition(UI_FRAME *self, int32_t x, int32_t y); -static void M_Draw(UI_FRAME *self); -static void M_Free(UI_FRAME *self); - -static int32_t M_GetWidth(const UI_FRAME *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return self->root->get_width(self->root) + self->margin * 2 - + self->padding * 2; -} - -static int32_t M_GetHeight(const UI_FRAME *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return self->root->get_height(self->root) + self->margin * 2 - + self->padding * 2; -} - -static void M_SetPosition( - UI_FRAME *const self, const int32_t x, const int32_t y) -{ - const int32_t w = M_GetWidth(self); - const int32_t h = M_GetHeight(self); - self->root->set_position( - self->root, x + self->margin + self->padding, - y + self->margin + self->padding); - Text_SetPos( - self->frame, x + self->margin, y + self->margin + TEXT_HEIGHT_FIXED); - self->frame->pos.z = 24; - Text_AddBackground( - self->frame, w - self->margin * 2, h - self->margin * 2, - (w - self->margin * 2) / 2, 0, TS_REQUESTED); - Text_AddOutline(self->frame, TS_REQUESTED); -} - -static void M_Draw(UI_FRAME *const self) -{ - if (self->vtable.is_hidden) { - return; - } - self->root->draw(self->root); - if (self->frame != nullptr && self->is_frame_visible) { - Text_DrawText(self->frame); - } -} - -static void M_Free(UI_FRAME *const self) -{ - Text_Remove(self->frame); - Memory_Free(self); -} - -UI_WIDGET *UI_Frame_Create( - UI_WIDGET *const root, const int32_t margin, const int32_t padding) -{ - UI_FRAME *const self = Memory_Alloc(sizeof(UI_FRAME)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = nullptr, - .draw = (UI_WIDGET_DRAW)M_Draw, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - self->root = root; - self->margin = margin; - self->padding = padding; - self->is_frame_visible = true; - self->frame = Text_Create(0, 0, ""); - self->frame->pos.z = -400; - self->frame->flags.manual_draw = 1; - return (UI_WIDGET *)self; -} - -void UI_Frame_SetFrameVisible( - UI_WIDGET *const widget, const bool is_frame_visible) -{ - UI_FRAME *const self = (UI_FRAME *)widget; - self->is_frame_visible = is_frame_visible; -} diff --git a/src/libtrx/game/ui/widgets/label.c b/src/libtrx/game/ui/widgets/label.c deleted file mode 100644 index 2740b6834..000000000 --- a/src/libtrx/game/ui/widgets/label.c +++ /dev/null @@ -1,164 +0,0 @@ -#include "game/ui/widgets/label.h" - -#include "game/text.h" -#include "memory.h" - -typedef struct { - UI_WIDGET_VTABLE vtable; - TEXTSTRING *text; - int32_t width; - int32_t height; - bool has_frame; -} UI_LABEL; - -static int32_t M_GetWidth(const UI_LABEL *self); -static int32_t M_GetHeight(const UI_LABEL *self); -static void M_SetPosition(UI_LABEL *self, int32_t x, int32_t y); -static void M_Draw(UI_LABEL *self); -static void M_Free(UI_LABEL *self); - -static int32_t M_GetWidth(const UI_LABEL *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - if (self->width != UI_LABEL_AUTO_SIZE) { - return self->width; - } - return Text_GetWidth(self->text); -} - -static int32_t M_GetHeight(const UI_LABEL *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - if (self->height != UI_LABEL_AUTO_SIZE) { - return self->height; - } - return Text_GetHeight(self->text); -} - -static void M_SetPosition( - UI_LABEL *const self, const int32_t x, const int32_t y) -{ - Text_SetPos(self->text, x, y + TEXT_HEIGHT_FIXED); -} - -static void M_Draw(UI_LABEL *const self) -{ - if (self->vtable.is_hidden) { - return; - } - Text_DrawText(self->text); -} - -static void M_Free(UI_LABEL *const self) -{ - Text_Remove(self->text); - Memory_Free(self); -} - -UI_WIDGET *UI_Label_Create( - const char *const text, const int32_t width, const int32_t height) -{ - UI_LABEL *self = Memory_Alloc(sizeof(UI_LABEL)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = nullptr, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->width = width; - self->height = height; - self->has_frame = false; - - self->text = Text_Create(0, 0, text); - self->text->pos.z = 16; - Text_SetMultiline(self->text, true); - self->text->flags.manual_draw = 1; - - return (UI_WIDGET *)self; -} - -void UI_Label_ChangeText(UI_WIDGET *const widget, const char *const text) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - Text_ChangeText(self->text, text); -} - -const char *UI_Label_GetText(UI_WIDGET *const widget) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - return self->text->content; -} - -void UI_Label_SetSize( - UI_WIDGET *const widget, const int32_t width, const int32_t height) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - self->width = width; - self->height = height; -} - -void UI_Label_SetVisible(UI_WIDGET *const widget, const bool visible) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - Text_Hide(self->text, !visible); -} - -void UI_Label_AddFrame(UI_WIDGET *const widget) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - if (!self->has_frame) { - self->text->pos.z = 0; - Text_AddBackground(self->text, 0, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(self->text, TS_REQUESTED); - self->has_frame = true; - } -} - -void UI_Label_RemoveFrame(UI_WIDGET *const widget) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - if (self->has_frame) { - Text_RemoveBackground(self->text); - Text_RemoveOutline(self->text); - self->text->pos.z = 16; - self->has_frame = false; - } -} - -void UI_Label_Flash( - UI_WIDGET *const widget, const bool enable, const int32_t rate) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - Text_Flash(self->text, enable, rate); -} - -void UI_Label_SetScale(UI_WIDGET *const widget, const float scale) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - Text_SetScale(self->text, TEXT_BASE_SCALE * scale, TEXT_BASE_SCALE * scale); -} - -void UI_Label_SetZIndex(UI_WIDGET *const widget, const int32_t z_index) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - self->text->pos.z = z_index; -} - -int32_t UI_Label_MeasureTextWidth(UI_WIDGET *const widget) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - return Text_GetWidth(self->text); -} - -int32_t UI_Label_MeasureTextHeight(UI_WIDGET *const widget) -{ - UI_LABEL *const self = (UI_LABEL *)widget; - return Text_GetHeight(self->text); -} diff --git a/src/libtrx/game/ui/widgets/photo_mode.c b/src/libtrx/game/ui/widgets/photo_mode.c deleted file mode 100644 index c88137456..000000000 --- a/src/libtrx/game/ui/widgets/photo_mode.c +++ /dev/null @@ -1,221 +0,0 @@ -#include "game/ui/widgets/photo_mode.h" - -#include "config.h" -#include "game/game_string.h" -#include "game/input.h" -#include "game/text.h" -#include "game/ui/common.h" -#include "game/ui/widgets/label.h" -#include "game/ui/widgets/spacer.h" -#include "game/ui/widgets/stack.h" -#include "game/ui/widgets/window.h" -#include "memory.h" - -#include - -#define TITLE_MARGIN 5 -#define WINDOW_MARGIN 10 -#define DIALOG_PADDING 5 - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *window; - UI_WIDGET *title; - UI_WIDGET *outer_stack; - UI_WIDGET *inner_stack; - UI_WIDGET *left_stack; - UI_WIDGET *right_stack; - UI_WIDGET *spacer; - bool shown; - int32_t label_count; - UI_WIDGET **left_labels; - UI_WIDGET **right_labels; -} UI_PHOTO_MODE; - -static int32_t M_GetWidth(const UI_PHOTO_MODE *self); -static int32_t M_GetHeight(const UI_PHOTO_MODE *self); -static void M_SetPosition(UI_PHOTO_MODE *self, int32_t x, int32_t y); -static void M_Control(UI_PHOTO_MODE *self); -static void M_Draw(UI_PHOTO_MODE *self); -static void M_Free(UI_PHOTO_MODE *self); - -static int32_t M_GetWidth(const UI_PHOTO_MODE *const self) -{ - return UI_GetCanvasWidth() - 2 * WINDOW_MARGIN; -} - -static int32_t M_GetHeight(const UI_PHOTO_MODE *const self) -{ - return UI_GetCanvasHeight() - 2 * WINDOW_MARGIN; -} - -static void M_SetPosition(UI_PHOTO_MODE *const self, int32_t x, int32_t y) -{ - self->window->set_position(self->window, x, y); -} - -static void M_Control(UI_PHOTO_MODE *const self) -{ - if (self->window->control != nullptr) { - self->window->control(self->window); - } - - self->shown = g_Config.ui.enable_photo_mode_ui; -} - -static void M_Draw(UI_PHOTO_MODE *const self) -{ - if (self->shown && self->window->draw != nullptr) { - self->window->draw(self->window); - } -} - -static void M_Free(UI_PHOTO_MODE *const self) -{ - for (int32_t i = 0; i < self->label_count; i++) { - self->left_labels[i]->free(self->left_labels[i]); - self->right_labels[i]->free(self->right_labels[i]); - } - self->spacer->free(self->spacer); - self->title->free(self->title); - self->outer_stack->free(self->outer_stack); - self->inner_stack->free(self->inner_stack); - self->left_stack->free(self->left_stack); - self->right_stack->free(self->right_stack); - self->window->free(self->window); - Memory_Free(self->left_labels); - Memory_Free(self->right_labels); - Memory_Free(self); -} - -UI_WIDGET *UI_PhotoMode_Create(void) -{ - UI_PHOTO_MODE *const self = Memory_Alloc(sizeof(UI_PHOTO_MODE)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->outer_stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - self->inner_stack = UI_Stack_Create( - UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - self->left_stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - self->right_stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - - const char *const title = GS(PHOTO_MODE_TITLE); - self->title = UI_Label_Create(title, UI_LABEL_AUTO_SIZE, TEXT_HEIGHT_FIXED); - UI_Stack_AddChild(self->outer_stack, self->title); - - self->spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN); - UI_Stack_AddChild(self->outer_stack, self->spacer); - UI_Stack_AddChild(self->outer_stack, self->inner_stack); - UI_Stack_AddChild(self->inner_stack, self->left_stack); - UI_Stack_AddChild(self->inner_stack, self->right_stack); - - char move_role[50]; - sprintf( - move_role, "%s%s%s%s%s%s: ", - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_CAMERA_UP), - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_CAMERA_DOWN), - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_CAMERA_FORWARD), - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_CAMERA_BACK), - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_CAMERA_LEFT), - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_CAMERA_RIGHT)); - - const char *const rot_role = - "\\{button left} \\{button up} " - "\\{button down} \\{button right} : "; - - char z_roll_role[100]; - sprintf(z_roll_role, "%s: ", GS(PHOTO_MODE_ROLL_ROLE)); - - char roll_role[100]; - sprintf(roll_role, "%s: ", GS(KEYMAP_ROLL)); - - char fov_role[100]; - sprintf(fov_role, "%s: ", GS(PHOTO_MODE_FOV_ROLE)); - - char reset_role[100]; - sprintf(reset_role, "%s: ", GS(KEYMAP_LOOK)); - - char help_role[20]; - sprintf( - help_role, "%s: ", - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_TOGGLE_UI)); - - char action_role[100]; - sprintf(action_role, "%s: ", GS(KEYMAP_ACTION)); - - char exit_role[100]; - sprintf( - exit_role, "%s/%s: ", - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_TOGGLE_PHOTO_MODE), - Input_GetKeyName( - INPUT_BACKEND_KEYBOARD, g_Config.input.keyboard_layout, - INPUT_ROLE_OPTION)); - - const char *const inputs[] = { - move_role, rot_role, z_roll_role, roll_role, fov_role, - reset_role, help_role, action_role, exit_role, nullptr, - }; - - const char *const roles[] = { - GS(PHOTO_MODE_MOVE_PROMPT), - GS(PHOTO_MODE_ROTATE_PROMPT), - GS(PHOTO_MODE_ROLL_PROMPT), - GS(PHOTO_MODE_ROTATE90_PROMPT), - GS(PHOTO_MODE_FOV_PROMPT), - GS(PHOTO_MODE_RESET_PROMPT), - GS(MISC_TOGGLE_HELP), - GS(PHOTO_MODE_SNAP_PROMPT), - GS(MISC_EXIT), - nullptr, - }; - - self->shown = true; - self->label_count = 0; - while (inputs[self->label_count] != nullptr) { - self->label_count++; - } - - self->left_labels = Memory_Alloc(sizeof(UI_WIDGET *) * self->label_count); - self->right_labels = Memory_Alloc(sizeof(UI_WIDGET *) * self->label_count); - for (int32_t i = 0; i < self->label_count; i++) { - self->left_labels[i] = - UI_Label_Create(inputs[i], UI_LABEL_AUTO_SIZE, TEXT_HEIGHT_FIXED); - UI_Stack_AddChild(self->left_stack, self->left_labels[i]); - self->right_labels[i] = - UI_Label_Create(roles[i], UI_LABEL_AUTO_SIZE, TEXT_HEIGHT_FIXED); - UI_Stack_AddChild(self->right_stack, self->right_labels[i]); - } - - self->window = UI_Window_Create( - self->outer_stack, DIALOG_PADDING, DIALOG_PADDING, DIALOG_PADDING * 2, - DIALOG_PADDING); - - M_SetPosition(self, WINDOW_MARGIN, WINDOW_MARGIN); - return (UI_WIDGET *)self; -} diff --git a/src/libtrx/game/ui/widgets/prompt.c b/src/libtrx/game/ui/widgets/prompt.c deleted file mode 100644 index 8dadf4fa0..000000000 --- a/src/libtrx/game/ui/widgets/prompt.c +++ /dev/null @@ -1,327 +0,0 @@ -#include "game/ui/widgets/prompt.h" - -#include "game/const.h" -#include "game/input.h" -#include "game/ui/common.h" -#include "game/ui/events.h" -#include "game/ui/widgets/label.h" -#include "memory.h" -#include "strings.h" - -#include - -static const char m_ValidPromptChars[] = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-: "; - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *label; - UI_WIDGET *caret; - - int32_t listener1; - int32_t listener2; - - struct { - int32_t x; - int32_t y; - } pos; - bool is_focused; - int32_t current_text_capacity; - char *current_text; - int32_t caret_pos; -} UI_PROMPT; - -static void M_UpdatePromptLabel(UI_PROMPT *self); -static void M_UpdateCaretLabel(UI_PROMPT *self); -static void M_MoveCaretLeft(UI_PROMPT *self); -static void M_MoveCaretRight(UI_PROMPT *self); -static void M_MoveCaretStart(UI_PROMPT *self); -static void M_MoveCaretEnd(UI_PROMPT *self); -static void M_DeleteCharBack(UI_PROMPT *self); -static void M_Confirm(UI_PROMPT *self); -static void M_Cancel(UI_PROMPT *self); -static void M_Clear(UI_PROMPT *self); - -static int32_t M_GetWidth(const UI_PROMPT *self); -static int32_t M_GetHeight(const UI_PROMPT *self); -static void M_SetPosition(UI_PROMPT *self, int32_t x, int32_t y); -static void M_Control(UI_PROMPT *self); -static void M_Draw(UI_PROMPT *self); -static void M_Free(UI_PROMPT *self); -static void M_HandleKeyDown(const EVENT *event, void *user_data); -static void M_HandleTextEdit(const EVENT *event, void *user_data); - -static void M_UpdatePromptLabel(UI_PROMPT *const self) -{ - UI_Label_ChangeText(self->label, self->current_text); -} - -static void M_UpdateCaretLabel(UI_PROMPT *const self) -{ - const char old = self->current_text[self->caret_pos]; - self->current_text[self->caret_pos] = '\0'; - UI_Label_ChangeText(self->label, self->current_text); - const int32_t width = UI_Label_MeasureTextWidth(self->label); - self->current_text[self->caret_pos] = old; - UI_Label_ChangeText(self->label, self->current_text); - - self->caret->set_position(self->caret, self->pos.x + width, self->pos.y); -} - -static int32_t M_GetWidth(const UI_PROMPT *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return self->label->get_width(self->label); -} - -static int32_t M_GetHeight(const UI_PROMPT *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return self->label->get_height(self->label); -} - -static void M_SetPosition( - UI_PROMPT *const self, const int32_t x, const int32_t y) -{ - self->pos.x = x; - self->pos.y = y; - self->label->set_position(self->label, x, y); - M_UpdateCaretLabel(self); -} - -static void M_Control(UI_PROMPT *const self) -{ - if (self->label->control != nullptr) { - self->label->control(self->label); - } - if (self->caret->control != nullptr) { - self->caret->control(self->caret); - } -} - -static void M_Draw(UI_PROMPT *const self) -{ - if (self->vtable.is_hidden) { - return; - } - if (self->label->draw != nullptr) { - self->label->draw(self->label); - } - if (self->caret->draw != nullptr) { - self->caret->draw(self->caret); - } -} - -static void M_Free(UI_PROMPT *const self) -{ - self->label->free(self->label); - self->caret->free(self->caret); - UI_Events_Unsubscribe(self->listener1); - UI_Events_Unsubscribe(self->listener2); - Memory_FreePointer(&self->current_text); - Memory_Free(self); -} - -static void M_MoveCaretLeft(UI_PROMPT *const self) -{ - if (self->caret_pos > 0) { - self->caret_pos--; - M_UpdateCaretLabel(self); - } -} - -static void M_MoveCaretRight(UI_PROMPT *const self) -{ - if (self->caret_pos < (int32_t)strlen(self->current_text)) { - self->caret_pos++; - M_UpdateCaretLabel(self); - } -} - -static void M_MoveCaretStart(UI_PROMPT *const self) -{ - self->caret_pos = 0; - M_UpdateCaretLabel(self); -} - -static void M_MoveCaretEnd(UI_PROMPT *const self) -{ - self->caret_pos = strlen(self->current_text); - M_UpdateCaretLabel(self); -} - -static void M_DeleteCharBack(UI_PROMPT *const self) -{ - if (self->caret_pos <= 0) { - return; - } - - memmove( - self->current_text + self->caret_pos - 1, - self->current_text + self->caret_pos, - strlen(self->current_text) + 1 - self->caret_pos); - - self->caret_pos--; - M_UpdatePromptLabel(self); - M_UpdateCaretLabel(self); -} - -static void M_Confirm(UI_PROMPT *const self) -{ - if (String_IsEmpty(self->current_text)) { - M_Cancel(self); - return; - } - const EVENT event = { - .name = "confirm", - .sender = self, - .data = self->current_text, - }; - UI_Events_Fire(&event); - M_Clear(self); - M_UpdateCaretLabel(self); -} - -static void M_Cancel(UI_PROMPT *const self) -{ - const EVENT event = { - .name = "cancel", - .sender = self, - .data = self->current_text, - }; - UI_Events_Fire(&event); - M_Clear(self); -} - -static void M_Clear(UI_PROMPT *const self) -{ - strcpy(self->current_text, ""); - self->caret_pos = 0; - M_UpdatePromptLabel(self); - M_UpdateCaretLabel(self); -} - -static void M_HandleKeyDown(const EVENT *const event, void *const user_data) -{ - const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data; - UI_PROMPT *const self = user_data; - - if (!self->is_focused) { - return; - } - - // clang-format off - switch (key) { - case UI_KEY_LEFT: M_MoveCaretLeft(self); break; - case UI_KEY_RIGHT: M_MoveCaretRight(self); break; - case UI_KEY_HOME: M_MoveCaretStart(self); break; - case UI_KEY_END: M_MoveCaretEnd(self); break; - case UI_KEY_BACK: M_DeleteCharBack(self); break; - case UI_KEY_RETURN: M_Confirm(self); break; - case UI_KEY_ESCAPE: M_Cancel(self); break; - default: break; - } - // clang-format on -} - -static void M_HandleTextEdit(const EVENT *const event, void *const user_data) -{ - const char *insert_string = event->data; - const size_t insert_length = strlen(insert_string); - UI_PROMPT *const self = user_data; - - if (!self->is_focused) { - return; - } - - if (strlen(insert_string) != 1 - || !strstr(m_ValidPromptChars, insert_string)) { - return; - } - - const size_t available_space = - self->current_text_capacity - strlen(self->current_text); - if (insert_length >= available_space) { - self->current_text_capacity *= 2; - self->current_text = - Memory_Realloc(self->current_text, self->current_text_capacity); - } - - memmove( - self->current_text + self->caret_pos + insert_length, - self->current_text + self->caret_pos, - strlen(self->current_text) + 1 - self->caret_pos); - memcpy(self->current_text + self->caret_pos, insert_string, insert_length); - - self->caret_pos += insert_length; - M_UpdatePromptLabel(self); - M_UpdateCaretLabel(self); -} - -UI_WIDGET *UI_Prompt_Create(const int32_t width, const int32_t height) -{ - UI_PROMPT *const self = Memory_Alloc(sizeof(UI_PROMPT)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->current_text_capacity = 1; - self->current_text = Memory_Alloc(self->current_text_capacity); - self->label = UI_Label_Create(self->current_text, width, height); - self->caret = UI_Label_Create("", width, height); - UI_Label_SetZIndex(self->label, 16); - UI_Label_SetZIndex(self->caret, 8); - self->is_focused = false; - - self->listener1 = - UI_Events_Subscribe("key_down", nullptr, M_HandleKeyDown, self); - self->listener2 = - UI_Events_Subscribe("text_edit", nullptr, M_HandleTextEdit, self); - - return (UI_WIDGET *)self; -} - -void UI_Prompt_SetSize( - UI_WIDGET *const widget, const int32_t width, const int32_t height) -{ - UI_PROMPT *const self = (UI_PROMPT *)widget; - UI_Label_SetSize(self->label, width, height); -} - -void UI_Prompt_SetFocus(UI_WIDGET *const widget, const bool is_focused) -{ - UI_PROMPT *const self = (UI_PROMPT *)widget; - self->is_focused = is_focused; - if (is_focused) { - Input_EnterListenMode(); - UI_Label_ChangeText(self->caret, "\\{button left}"); - UI_Label_Flash(self->caret, 1, LOGIC_FPS * 2 / 3); - } else { - Input_ExitListenMode(); - UI_Label_ChangeText(self->caret, ""); - } -} - -void UI_Prompt_Clear(UI_WIDGET *const widget) -{ - UI_PROMPT *const self = (UI_PROMPT *)widget; - M_Clear(self); -} - -void UI_Prompt_ChangeText(UI_WIDGET *widget, const char *new_text) -{ - UI_PROMPT *const self = (UI_PROMPT *)widget; - Memory_FreePointer(&self->current_text); - self->current_text = Memory_DupStr(new_text); - self->caret_pos = strlen(new_text); - M_UpdateCaretLabel(self); -} diff --git a/src/libtrx/game/ui/widgets/requester.c b/src/libtrx/game/ui/widgets/requester.c deleted file mode 100644 index b519c4bb9..000000000 --- a/src/libtrx/game/ui/widgets/requester.c +++ /dev/null @@ -1,321 +0,0 @@ -#include "game/ui/widgets/requester.h" - -#include "game/game_string.h" -#include "game/input.h" -#include "game/text.h" -#include "game/ui/common.h" -#include "game/ui/widgets/frame.h" -#include "game/ui/widgets/label.h" -#include "game/ui/widgets/stack.h" -#include "game/ui/widgets/window.h" -#include "log.h" -#include "memory.h" - -#include - -typedef struct { - void *user_data; - UI_WIDGET *frame; - UI_WIDGET *stack; - UI_WIDGET *left_label; - UI_WIDGET *right_label; -} M_ROW; - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *window; - UI_WIDGET *outer_stack; - UI_REQUESTER_SETTINGS settings; - - bool is_confirmed; - int32_t selected_row_offset; - int32_t visible_row_offset; - int32_t row_count; - M_ROW *rows; - - int32_t selection_margin; - int32_t selection_padding; - - int32_t listener; -} UI_REQUESTER; - -static void M_ClearRows(UI_REQUESTER *self); -static M_ROW *M_AddRow( - UI_REQUESTER *self, const char *left_text, const char *right_text, - void *user_data); -static void M_DoLayout(UI_REQUESTER *self); -static void M_HandleCanvasResize(const EVENT *event, void *data); - -static int32_t M_GetWidth(const UI_REQUESTER *self); -static int32_t M_GetHeight(const UI_REQUESTER *self); -static void M_SetPosition(UI_REQUESTER *self, int32_t x, int32_t y); -static void M_Control(UI_REQUESTER *self); -static void M_Draw(UI_REQUESTER *self); -static void M_Free(UI_REQUESTER *self); - -static void M_ClearRows(UI_REQUESTER *const self) -{ - for (int32_t i = 0; i < self->row_count; i++) { - self->rows[i].left_label->free(self->rows[i].left_label); - if (self->rows[i].right_label != nullptr) { - self->rows[i].right_label->free(self->rows[i].right_label); - } - self->rows[i].frame->free(self->rows[i].frame); - self->rows[i].stack->free(self->rows[i].stack); - } - UI_Stack_ClearChildren(self->outer_stack); - self->visible_row_offset = 0; - self->row_count = 0; - self->selected_row_offset = -1; - self->is_confirmed = false; -} - -static M_ROW *M_AddRow( - UI_REQUESTER *const self, const char *const left_text, - const char *const right_text, void *const user_data) -{ - self->row_count++; - self->rows = Memory_Realloc(self->rows, sizeof(M_ROW) * self->row_count); - M_ROW *const row = &self->rows[self->row_count - 1]; - - row->stack = UI_Stack_Create( - UI_STACK_LAYOUT_HORIZONTAL, self->settings.width, UI_STACK_AUTO_SIZE); - UI_Stack_SetHAlign(row->stack, UI_STACK_H_ALIGN_DISTRIBUTE); - - row->frame = UI_Frame_Create(row->stack, 0, 0); - UI_Frame_SetFrameVisible(row->frame, false); - - row->left_label = UI_Label_Create( - left_text, UI_LABEL_AUTO_SIZE, self->settings.row_height); - UI_Stack_AddChild(row->stack, row->left_label); - - if (right_text != nullptr) { - row->right_label = UI_Label_Create( - right_text, UI_LABEL_AUTO_SIZE, self->settings.row_height); - UI_Stack_AddChild(row->stack, row->right_label); - } else { - row->right_label = nullptr; - UI_Stack_SetHAlign(row->stack, UI_STACK_H_ALIGN_CENTER); - } - - row->user_data = user_data; - - for (int32_t y = 0; y < self->row_count; y++) { - self->rows[y].stack->is_hidden = y < self->visible_row_offset - || y >= self->visible_row_offset + self->settings.visible_rows; - } - if (self->settings.is_selectable && self->selected_row_offset == -1) { - self->selected_row_offset = 0; - UI_Frame_SetFrameVisible( - self->rows[self->selected_row_offset].frame, true); - } - - UI_Stack_AddChild(self->outer_stack, row->frame); - return row; -} - -static void M_DoLayout(UI_REQUESTER *const self) -{ - UI_HandleLayoutChange(); -} - -static void M_HandleCanvasResize(const EVENT *event, void *data) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)data; - M_DoLayout(self); -} - -static int32_t M_GetWidth(const UI_REQUESTER *const self) -{ - return self->window->get_width(self->window); -} - -static int32_t M_GetHeight(const UI_REQUESTER *const self) -{ - return self->window->get_height(self->window); -} - -static void M_SetPosition( - UI_REQUESTER *const self, const int32_t x, const int32_t y) -{ - self->window->set_position(self->window, x, y); -} - -static void M_Control(UI_REQUESTER *const self) -{ - if (self->window->control != nullptr) { - self->window->control(self->window); - } - - bool update = false; - if (g_InputDB.menu_down) { - if (self->visible_row_offset + self->settings.visible_rows - < self->row_count) { - self->rows[self->visible_row_offset].stack->is_hidden = true; - self->rows[self->visible_row_offset + self->settings.visible_rows] - .stack->is_hidden = false; - self->visible_row_offset++; - update = true; - } - } else if (g_InputDB.menu_up) { - if (self->visible_row_offset > 0) { - self->rows - [self->visible_row_offset + self->settings.visible_rows - 1] - .stack->is_hidden = true; - self->rows[self->visible_row_offset - 1].stack->is_hidden = false; - self->visible_row_offset--; - update = true; - } - } - - if (self->settings.is_selectable) { - if (g_InputDB.menu_down - && self->selected_row_offset + 1 < self->row_count) { - if (self->selected_row_offset != -1) { - UI_Frame_SetFrameVisible( - self->rows[self->selected_row_offset].frame, false); - } - self->selected_row_offset++; - UI_Frame_SetFrameVisible( - self->rows[self->selected_row_offset].frame, true); - update = true; - } else if (g_InputDB.menu_up && self->selected_row_offset > 0) { - if (self->selected_row_offset != -1) { - UI_Frame_SetFrameVisible( - self->rows[self->selected_row_offset].frame, false); - } - self->selected_row_offset--; - UI_Frame_SetFrameVisible( - self->rows[self->selected_row_offset].frame, true); - update = true; - } - if (g_InputDB.menu_confirm) { - self->is_confirmed = true; - } - } - - if (update) { - M_DoLayout(self); - } -} - -static void M_Draw(UI_REQUESTER *const self) -{ - if (self->window->draw != nullptr) { - self->window->draw(self->window); - } -} - -static void M_Free(UI_REQUESTER *const self) -{ - M_ClearRows(self); - self->outer_stack->free(self->outer_stack); - self->window->free(self->window); - UI_Events_Unsubscribe(self->listener); - Memory_Free(self); -} - -UI_WIDGET *UI_Requester_Create(UI_REQUESTER_SETTINGS settings) -{ - UI_REQUESTER *const self = Memory_Alloc(sizeof(UI_REQUESTER)); - if (settings.row_height == 0) { - settings.row_height = 18; - } - if (settings.width == 0) { - settings.width = UI_STACK_AUTO_SIZE; - } - - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->settings = settings; - self->outer_stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, - settings.row_height * self->settings.visible_rows); - UI_Stack_SetHAlign(self->outer_stack, UI_STACK_H_ALIGN_CENTER); - - self->window = UI_Window_Create(self->outer_stack, 8, 8, 8, 8); - - self->selected_row_offset = -1; - self->listener = UI_Events_Subscribe( - "canvas_resize", nullptr, M_HandleCanvasResize, self); - - M_DoLayout(self); - return (UI_WIDGET *)self; -} - -int32_t UI_Requester_GetSelectedRow(UI_WIDGET *const widget) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - return self->is_confirmed ? self->selected_row_offset : -1; -} - -void UI_Requester_SetTitle(UI_WIDGET *const widget, const char *const title) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - UI_Window_SetTitle(self->window, title); - M_DoLayout(self); -} - -void UI_Requester_ClearRows(UI_WIDGET *const widget) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - M_ClearRows(self); - M_DoLayout(self); -} - -void UI_Requester_AddRowLR( - UI_WIDGET *const widget, const char *const text_l, const char *const text_r, - void *const user_data) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - M_AddRow(self, text_l, text_r, user_data); - M_DoLayout(self); -} - -void UI_Requester_AddRowC( - UI_WIDGET *const widget, const char *const text, void *const user_data) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - M_AddRow(self, text, nullptr, user_data); - M_DoLayout(self); -} - -void *UI_Requester_GetRowUserData(UI_WIDGET *const widget, const int32_t idx) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - if (idx >= self->row_count || idx < 0) { - return nullptr; - } - return self->rows[idx].user_data; -} - -int32_t UI_Requester_GetRowCount(UI_WIDGET *const widget) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - return self->row_count; -} - -void UI_Requester_ChangeRowLR( - UI_WIDGET *widget, int32_t idx, const char *text_l, const char *text_r, - void *user_data) -{ - UI_REQUESTER *const self = (UI_REQUESTER *)widget; - if (idx >= self->row_count || idx < 0) { - return; - } - if (self->rows[idx].left_label != nullptr && text_l != nullptr) { - UI_Label_ChangeText(self->rows[idx].left_label, text_l); - } - if (self->rows[idx].right_label != nullptr && text_r != nullptr) { - UI_Label_ChangeText(self->rows[idx].right_label, text_r); - } - self->rows[idx].user_data = user_data; - M_DoLayout(self); -} diff --git a/src/libtrx/game/ui/widgets/spacer.c b/src/libtrx/game/ui/widgets/spacer.c deleted file mode 100644 index 0c06f853d..000000000 --- a/src/libtrx/game/ui/widgets/spacer.c +++ /dev/null @@ -1,65 +0,0 @@ -#include "game/ui/widgets/spacer.h" - -#include "memory.h" - -typedef struct { - UI_WIDGET_VTABLE vtable; - int32_t width; - int32_t height; -} UI_SPACER; - -static int32_t M_GetWidth(const UI_SPACER *self); -static int32_t M_GetHeight(const UI_SPACER *self); -static void M_SetPosition(UI_SPACER *self, int32_t x, int32_t y); -static void M_Control(UI_SPACER *self); -static void M_Free(UI_SPACER *self); - -static int32_t M_GetWidth(const UI_SPACER *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return self->width; -} - -static int32_t M_GetHeight(const UI_SPACER *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - return self->height; -} - -static void M_SetPosition( - UI_SPACER *const self, const int32_t x, const int32_t y) -{ -} - -static void M_Free(UI_SPACER *const self) -{ - Memory_Free(self); -} - -UI_WIDGET *UI_Spacer_Create(const int32_t width, const int32_t height) -{ - UI_SPACER *const self = Memory_Alloc(sizeof(UI_SPACER)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = nullptr, - .draw = nullptr, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - self->width = width; - self->height = height; - return (UI_WIDGET *)self; -} - -void UI_Spacer_SetSize( - UI_WIDGET *const widget, const int32_t width, const int32_t height) -{ - UI_SPACER *const self = (UI_SPACER *)widget; - self->width = width; - self->height = height; -} diff --git a/src/libtrx/game/ui/widgets/stack.c b/src/libtrx/game/ui/widgets/stack.c deleted file mode 100644 index 99a537a9b..000000000 --- a/src/libtrx/game/ui/widgets/stack.c +++ /dev/null @@ -1,314 +0,0 @@ -#include "game/ui/widgets/stack.h" - -#include "memory.h" -#include "utils.h" -#include "vector.h" - -typedef struct { - UI_WIDGET_VTABLE vtable; - - struct { - UI_STACK_H_ALIGN h; - UI_STACK_V_ALIGN v; - } align; - int32_t width; - int32_t height; - int32_t x; - int32_t y; - UI_STACK_LAYOUT layout; - VECTOR *children; -} UI_STACK; - -static int32_t M_GetChildrenWidth(const UI_STACK *self); -static int32_t M_GetChildrenHeight(const UI_STACK *self); -static int32_t M_GetHeight(const UI_STACK *self); -static int32_t M_GetWidth(const UI_STACK *self); -static void M_SetPosition(UI_STACK *self, int32_t x, int32_t y); -static void M_Control(UI_STACK *self); -static void M_Draw(UI_STACK *self); -static void M_Free(UI_STACK *self); - -static int32_t M_GetChildrenWidth(const UI_STACK *const self) -{ - int32_t result = 0; - for (int32_t i = 0; i < self->children->count; i++) { - const UI_WIDGET *const child = - *(UI_WIDGET **)Vector_Get(self->children, i); - switch (self->layout) { - case UI_STACK_LAYOUT_HORIZONTAL: - result += child->get_width(child); - break; - case UI_STACK_LAYOUT_VERTICAL: - result = MAX(result, child->get_width(child)); - break; - } - } - return result; -} - -static int32_t M_GetChildrenHeight(const UI_STACK *const self) -{ - int32_t result = 0; - for (int32_t i = 0; i < self->children->count; i++) { - const UI_WIDGET *const child = - *(UI_WIDGET **)Vector_Get(self->children, i); - switch (self->layout) { - case UI_STACK_LAYOUT_HORIZONTAL: - result = MAX(result, child->get_height(child)); - break; - case UI_STACK_LAYOUT_VERTICAL: - result += child->get_height(child); - break; - } - } - return result; -} - -static int32_t M_GetWidth(const UI_STACK *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - if (self->width != UI_STACK_AUTO_SIZE) { - return self->width; - } - return M_GetChildrenWidth(self); -} - -static int32_t M_GetHeight(const UI_STACK *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - if (self->height != UI_STACK_AUTO_SIZE) { - return self->height; - } - return M_GetChildrenHeight(self); -} - -static void M_SetPosition( - UI_STACK *const self, const int32_t x, const int32_t y) -{ - self->x = x; - self->y = y; - UI_Stack_DoLayout((UI_WIDGET *)self); -} - -static void M_Control(UI_STACK *const self) -{ - for (int32_t i = 0; i < self->children->count; i++) { - UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i); - if (child->control != nullptr) { - child->control(child); - } - } -} - -static void M_Draw(UI_STACK *const self) -{ - if (self->vtable.is_hidden) { - return; - } - for (int32_t i = 0; i < self->children->count; i++) { - UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i); - if (child->draw != nullptr) { - child->draw(child); - } - } -} - -static void M_Free(UI_STACK *const self) -{ - Vector_Free(self->children); - Memory_Free(self); -} - -void UI_Stack_ClearChildren(UI_WIDGET *const widget) -{ - UI_STACK *const self = (UI_STACK *)widget; - Vector_Clear(self->children); -} - -void UI_Stack_AddChild(UI_WIDGET *const widget, UI_WIDGET *const child) -{ - UI_STACK *const self = (UI_STACK *)widget; - Vector_Add(self->children, (void *)&child); -} - -UI_WIDGET *UI_Stack_Create( - const UI_STACK_LAYOUT layout, const int32_t width, const int32_t height) -{ - UI_STACK *const self = Memory_Alloc(sizeof(UI_STACK)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->align.h = UI_STACK_H_ALIGN_LEFT; - self->align.v = UI_STACK_V_ALIGN_TOP; - self->width = width; - self->height = height; - self->layout = layout; - self->children = Vector_Create(sizeof(UI_WIDGET *)); - return (UI_WIDGET *)self; -} - -void UI_Stack_SetHAlign(UI_WIDGET *const widget, const UI_STACK_H_ALIGN align) -{ - UI_STACK *const self = (UI_STACK *)widget; - self->align.h = align; -} - -void UI_Stack_SetVAlign(UI_WIDGET *const widget, const UI_STACK_V_ALIGN align) -{ - UI_STACK *const self = (UI_STACK *)widget; - self->align.v = align; -} - -void UI_Stack_SetSize( - UI_WIDGET *const widget, const int32_t width, const int32_t height) -{ - UI_STACK *const self = (UI_STACK *)widget; - self->width = width; - self->height = height; - UI_Stack_DoLayout(widget); -} - -void UI_Stack_DoLayout(UI_WIDGET *const widget) -{ - UI_STACK *const self = (UI_STACK *)widget; - const int32_t self_width = M_GetWidth(self); - const int32_t self_height = M_GetHeight(self); - const int32_t children_width = M_GetChildrenWidth(self); - const int32_t children_height = M_GetChildrenHeight(self); - - // calculate main axis placement - int32_t x = -999; - int32_t y = -999; - - int32_t spacing_h = 0; - int32_t remainder_h = 0; - if (self->children->count > 1 && self->layout == UI_STACK_LAYOUT_HORIZONTAL - && self->align.h == UI_STACK_H_ALIGN_DISTRIBUTE - && self_width > children_width) { - spacing_h = (self_width - children_width) / (self->children->count - 1); - remainder_h = - (self_width - children_width) % (self->children->count - 1); - } - - int32_t spacing_v = 0; - int32_t remainder_v = 0; - if (self->children->count > 1 && self->layout == UI_STACK_LAYOUT_VERTICAL - && self->align.v == UI_STACK_V_ALIGN_DISTRIBUTE - && self_height > children_height) { - spacing_v = - (self_height - children_height) / (self->children->count - 1); - remainder_v = - (self_height - children_height) % (self->children->count - 1); - } - - switch (self->layout) { - case UI_STACK_LAYOUT_HORIZONTAL: - switch (self->align.h) { - case UI_STACK_H_ALIGN_LEFT: - x = self->x; - break; - case UI_STACK_H_ALIGN_CENTER: - x = self->x + (self_width - children_width) / 2; - break; - case UI_STACK_H_ALIGN_RIGHT: - x = self->x + self_width - children_width; - break; - case UI_STACK_H_ALIGN_DISTRIBUTE: - if (self->children->count == 1) { - x = self->x + (self_width - children_width) / 2; - } else { - x = self->x; - } - break; - } - break; - - case UI_STACK_LAYOUT_VERTICAL: - switch (self->align.v) { - case UI_STACK_V_ALIGN_TOP: - y = self->y; - break; - case UI_STACK_V_ALIGN_CENTER: - y = self->y + (self_height - children_height) / 2; - break; - case UI_STACK_V_ALIGN_BOTTOM: - y = self->y + self_height - children_height; - break; - case UI_STACK_V_ALIGN_DISTRIBUTE: - y = self->y; - break; - } - break; - } - - for (int32_t i = 0; i < self->children->count; i++) { - UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i); - const int32_t child_width = child->get_width(child); - const int32_t child_height = child->get_height(child); - - // calculate other axis placement - switch (self->layout) { - case UI_STACK_LAYOUT_HORIZONTAL: - switch (self->align.v) { - case UI_STACK_V_ALIGN_TOP: - y = self->y; - break; - case UI_STACK_V_ALIGN_CENTER: - y = self->y + (self_height - child_height) / 2; - break; - case UI_STACK_V_ALIGN_BOTTOM: - y = self->y + self_height - child_height; - break; - default: - break; - } - break; - - case UI_STACK_LAYOUT_VERTICAL: - switch (self->align.h) { - case UI_STACK_H_ALIGN_LEFT: - x = self->x; - break; - case UI_STACK_H_ALIGN_CENTER: - x = self->x + (self_width - child_width) / 2; - break; - case UI_STACK_H_ALIGN_RIGHT: - x = self->x + self_width - child_width; - break; - default: - break; - } - break; - } - - child->set_position(child, x, y); - - // calculate main axis offset - switch (self->layout) { - case UI_STACK_LAYOUT_HORIZONTAL: - x += child_width + spacing_h; - if (remainder_h > 0) { - x += 1; - remainder_h--; - } - break; - case UI_STACK_LAYOUT_VERTICAL: - y += child_height + spacing_v; - if (remainder_v > 0) { - y += 1; - remainder_v--; - } - break; - } - } -} diff --git a/src/libtrx/game/ui/widgets/window.c b/src/libtrx/game/ui/widgets/window.c deleted file mode 100644 index 12b6db4e7..000000000 --- a/src/libtrx/game/ui/widgets/window.c +++ /dev/null @@ -1,177 +0,0 @@ -#include "game/ui/widgets/window.h" - -#include "game/text.h" -#include "game/ui/widgets/label.h" -#include "memory.h" -#include "utils.h" - -typedef struct { - UI_WIDGET_VTABLE vtable; - TEXTSTRING *frame; - TEXTSTRING *title_frame; - UI_WIDGET *root; - UI_WIDGET *title_label; - struct { - int32_t left; - int32_t right; - int32_t top; - int32_t bottom; - } border; - int32_t title_margin; - int32_t title_padding; -} UI_WINDOW; - -static int32_t M_GetWidth(const UI_WINDOW *self); -static int32_t M_GetHeight(const UI_WINDOW *self); -static void M_SetPosition(UI_WINDOW *self, int32_t x, int32_t y); -static void M_Control(UI_WINDOW *self); -static void M_Draw(UI_WINDOW *self); -static void M_Free(UI_WINDOW *self); - -static int32_t M_GetWidth(const UI_WINDOW *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - const int32_t title_width = self->title_label != nullptr - ? self->title_label->get_width(self->title_label) - + 2 * self->title_margin + 2 * self->title_padding - : 0; - const int32_t root_width = self->root->get_width(self->root) - + self->border.left + self->border.right; - return MAX(title_width, root_width); -} - -static int32_t M_GetHeight(const UI_WINDOW *const self) -{ - if (self->vtable.is_hidden) { - return 0; - } - const int32_t title_height = self->title_label != nullptr - ? self->title_label->get_height(self->title_label) - + 2 * self->title_margin + 2 * self->title_padding - : 0; - const int32_t root_height = self->root->get_height(self->root) - + self->border.top + self->border.bottom; - return title_height + root_height; -} - -static void M_SetPosition( - UI_WINDOW *const self, const int32_t x, const int32_t y) -{ - const int32_t w = M_GetWidth(self); - const int32_t h = M_GetHeight(self); - - if (self->title_label != nullptr) { - self->title_label->set_position( - self->title_label, - x + (w - self->title_label->get_width(self->title_label)) / 2, - y + self->title_margin + self->title_padding); - - self->root->set_position( - self->root, x + (w - self->root->get_width(self->root)) / 2, - y + self->title_label->get_height(self->title_label) - + self->title_margin * 2 + self->title_padding * 2 - + self->border.top); - - Text_SetPos( - self->title_frame, x + self->title_margin, - y + TEXT_HEIGHT_FIXED + self->title_margin); - Text_AddBackground( - self->title_frame, w - self->title_margin * 2, - TEXT_HEIGHT_FIXED + self->title_padding * 2, - (w - self->title_margin * 2) / 2, 0, TS_REQUESTED); - Text_AddOutline(self->title_frame, TS_REQUESTED); - } else { - self->root->set_position( - self->root, x + self->border.left, y + self->border.top); - } - - Text_SetPos(self->frame, x, y + TEXT_HEIGHT_FIXED); - Text_AddBackground(self->frame, w, h, w / 2, 0, TS_BACKGROUND); - Text_AddOutline(self->frame, TS_BACKGROUND); -} - -static void M_Control(UI_WINDOW *const self) -{ - if (self->root->control != nullptr) { - self->root->control(self->root); - } -} - -static void M_Draw(UI_WINDOW *const self) -{ - if (self->vtable.is_hidden) { - return; - } - Text_DrawText(self->frame); - if (self->root->draw != nullptr) { - self->root->draw(self->root); - } - if (self->title_label != nullptr) { - self->title_label->draw(self->title_label); - Text_DrawText(self->title_frame); - } -} - -static void M_Free(UI_WINDOW *const self) -{ - if (self->title_label != nullptr) { - self->title_label->free(self->title_label); - Text_Remove(self->title_frame); - } - Text_Remove(self->frame); - Memory_Free(self); -} - -UI_WIDGET *UI_Window_Create( - UI_WIDGET *const root, const int32_t border_top, const int32_t border_right, - const int32_t border_bottom, const int32_t border_left) -{ - UI_WINDOW *const self = Memory_Alloc(sizeof(UI_WINDOW)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->root = root; - self->border.top = border_top; - self->border.right = border_right; - self->border.bottom = border_bottom; - self->border.left = border_left; - self->title_margin = 2; - self->title_padding = 2; - - self->frame = Text_Create(0, 0, ""); - self->frame->pos.z = 32; - self->frame->flags.manual_draw = 1; - - return (UI_WIDGET *)self; -} - -void UI_Window_SetTitle(UI_WIDGET *const widget, const char *const text) -{ - UI_WINDOW *const self = (UI_WINDOW *)widget; - if (self->title_label != nullptr) { - self->title_label->free(self->title_label); - self->title_label = nullptr; - Text_Remove(self->title_frame); - } - if (text != nullptr) { - self->title_label = - UI_Label_Create(text, UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE); - self->title_frame = Text_Create(0, 0, ""); - self->title_frame->pos.z = 16; - self->title_frame->flags.manual_draw = 1; - } -} - -void UI_Window_SetRootWidget(UI_WIDGET *const widget, UI_WIDGET *const root) -{ - UI_WINDOW *const self = (UI_WINDOW *)widget; - self->root = root; -} diff --git a/src/libtrx/gfx/3d/3d_renderer.c b/src/libtrx/gfx/3d/3d_renderer.c index 1f1f6d1d7..9388b390b 100644 --- a/src/libtrx/gfx/3d/3d_renderer.c +++ b/src/libtrx/gfx/3d/3d_renderer.c @@ -22,7 +22,6 @@ struct GFX_3D_RENDERER { // shader variable locations GLint loc_mat_projection; - GLint loc_mat_model_view; GLint loc_texturing_enabled; GLint loc_smoothing_enabled; GLint loc_alpha_point_discard; @@ -142,8 +141,6 @@ GFX_3D_RENDERER *GFX_3D_Renderer_Create(void) renderer->loc_mat_projection = GFX_GL_Program_UniformLocation(&renderer->program, "matProjection"); - renderer->loc_mat_model_view = - GFX_GL_Program_UniformLocation(&renderer->program, "matModelView"); renderer->loc_texturing_enabled = GFX_GL_Program_UniformLocation(&renderer->program, "texturingEnabled"); renderer->loc_smoothing_enabled = diff --git a/src/libtrx/gfx/context.c b/src/libtrx/gfx/context.c index 6100f6e77..aafdf8f05 100644 --- a/src/libtrx/gfx/context.c +++ b/src/libtrx/gfx/context.c @@ -63,6 +63,9 @@ static GLvoid GLAPIENTRY M_GLDebug( const GLenum severity, const GLsizei length, const GLchar *const message, const void *const user_param) { + if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) { + return; + } LOG_INFO("%d %s", source, message); } @@ -158,7 +161,7 @@ bool GFX_Context_Attach(void *window_handle, GFX_GL_BACKEND backend) // VSync defaults to on unless user disabled it in runtime json SDL_GL_SetSwapInterval(1); -#if DEBUG +#if DEBUG && !defined(__APPLE__) glDebugMessageCallback(M_GLDebug, nullptr); glEnable(GL_DEBUG_OUTPUT); #endif @@ -331,11 +334,6 @@ void GFX_Context_ClearScheduledScreenshotPath(void) Memory_FreePointer(&m_Context.scheduled_screenshot_path); } -void GFX_Context_GetScale(float *const out_x, float *const out_y) -{ - m_Context.renderer->get_scale(m_Context.renderer, out_x, out_y); -} - GFX_CONFIG *GFX_Context_GetConfig(void) { return &m_Context.config; diff --git a/src/libtrx/gfx/gl/program.c b/src/libtrx/gfx/gl/program.c index ff1d30c24..71950aa77 100644 --- a/src/libtrx/gfx/gl/program.c +++ b/src/libtrx/gfx/gl/program.c @@ -32,7 +32,7 @@ void GFX_GL_Program_Close(GFX_GL_PROGRAM *program) } } -void GFX_GL_Program_Bind(GFX_GL_PROGRAM *program) +void GFX_GL_Program_Bind(const GFX_GL_PROGRAM *const program) { ASSERT(program != nullptr); glUseProgram(program->id); diff --git a/src/libtrx/gfx/renderers/fbo_renderer.c b/src/libtrx/gfx/renderers/fbo_renderer.c index c6c3a2a06..f00fd204e 100644 --- a/src/libtrx/gfx/renderers/fbo_renderer.c +++ b/src/libtrx/gfx/renderers/fbo_renderer.c @@ -34,8 +34,6 @@ static void M_SwapBuffers(GFX_RENDERER *renderer); static void M_Init(GFX_RENDERER *renderer, const GFX_CONFIG *config); static void M_Shutdown(GFX_RENDERER *renderer); static void M_Reset(GFX_RENDERER *renderer); -static void M_GetScale( - const GFX_RENDERER *renderer, float *out_scale_x, float *out_scale_y); static void M_Render(GFX_RENDERER *renderer); static void M_Bind(const GFX_RENDERER *renderer); @@ -246,18 +244,9 @@ static void M_Unbind(const GFX_RENDERER *renderer) glBindFramebuffer(GL_FRAMEBUFFER, 0); } -static void M_GetScale( - const GFX_RENDERER *renderer, float *const out_scale_x, - float *const out_scale_y) -{ - *out_scale_x = 1.0f; - *out_scale_y = 1.0f; -} - GFX_RENDERER g_GFX_Renderer_FBO = { .swap_buffers = &M_SwapBuffers, .init = &M_Init, .shutdown = &M_Shutdown, .reset = &M_Reset, - .get_scale = M_GetScale, }; diff --git a/src/libtrx/gfx/renderers/legacy_renderer.c b/src/libtrx/gfx/renderers/legacy_renderer.c index 24f60641c..4553465c4 100644 --- a/src/libtrx/gfx/renderers/legacy_renderer.c +++ b/src/libtrx/gfx/renderers/legacy_renderer.c @@ -28,21 +28,10 @@ static void M_SwapBuffers(GFX_RENDERER *renderer) GFX_GL_CheckError(); } -static void M_GetScale( - const GFX_RENDERER *renderer, float *const out_scale_x, - float *const out_scale_y) -{ - const int32_t window_width = GFX_Context_GetWindowWidth(); - const int32_t window_height = GFX_Context_GetWindowHeight(); - *out_scale_x = window_width / (float)GFX_Context_GetDisplayWidth(); - *out_scale_y = window_height / (float)GFX_Context_GetDisplayHeight(); -} - GFX_RENDERER g_GFX_Renderer_Legacy = { .priv = nullptr, .swap_buffers = &M_SwapBuffers, .init = nullptr, .reset = nullptr, .shutdown = nullptr, - .get_scale = M_GetScale, }; diff --git a/src/libtrx/include/libtrx/colors.h b/src/libtrx/include/libtrx/colors.h new file mode 100644 index 000000000..b52af4227 --- /dev/null +++ b/src/libtrx/include/libtrx/colors.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +typedef struct { + float r; + float g; + float b; +} RGB_F; + +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} RGB_888; + +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +} RGBA_8888; diff --git a/src/libtrx/include/libtrx/config/const.h b/src/libtrx/include/libtrx/config/const.h new file mode 100644 index 000000000..a10d9c40e --- /dev/null +++ b/src/libtrx/include/libtrx/config/const.h @@ -0,0 +1,3 @@ +#pragma once + +#define MAX_ASSAULT_TIMES 10 diff --git a/src/libtrx/include/libtrx/config/option.h b/src/libtrx/include/libtrx/config/option.h index 12d3c461b..c5a32928c 100644 --- a/src/libtrx/include/libtrx/config/option.h +++ b/src/libtrx/include/libtrx/config/option.h @@ -1,11 +1,12 @@ #pragma once typedef enum { - COT_BOOL = 0, - COT_INT32 = 1, - COT_FLOAT = 2, - COT_DOUBLE = 3, - COT_ENUM = 4, + COT_BOOL, + COT_INT32, + COT_FLOAT, + COT_DOUBLE, + COT_ENUM, + COT_RGB888, } CONFIG_OPTION_TYPE; typedef struct { diff --git a/src/libtrx/include/libtrx/config/types.h b/src/libtrx/include/libtrx/config/types.h index 185abd51c..a10346e43 100644 --- a/src/libtrx/include/libtrx/config/types.h +++ b/src/libtrx/include/libtrx/config/types.h @@ -1,5 +1,28 @@ #pragma once +#include "./const.h" + +#include + +typedef enum { + MUSIC_LOAD_NEVER, + MUSIC_LOAD_NON_AMBIENT, + MUSIC_LOAD_ALWAYS, +} MUSIC_LOAD_CONDITION; + +typedef enum { + UI_STYLE_PS1, + UI_STYLE_PC, +} UI_STYLE; + +typedef struct { + struct { + uint32_t time; + uint32_t attempt_num; + } entries[MAX_ASSAULT_TIMES]; + uint32_t total_attempts; +} ASSAULT_STATS; + #if TR_VERSION == 1 #include "./types_tr1.h" #elif TR_VERSION == 2 diff --git a/src/libtrx/include/libtrx/config/types_tr1.h b/src/libtrx/include/libtrx/config/types_tr1.h index 01c907aec..81a463c41 100644 --- a/src/libtrx/include/libtrx/config/types_tr1.h +++ b/src/libtrx/include/libtrx/config/types_tr1.h @@ -1,11 +1,11 @@ #pragma once +#include "../game/gym.h" +#include "../game/output/types.h" #include "../game/sound/enum.h" #include "../gfx/common.h" #include "../screenshot.h" -#include - #define CONFIG_MIN_BRIGHTNESS 0.1f #define CONFIG_MAX_BRIGHTNESS 2.0f #define CONFIG_MIN_TEXT_SCALE 0.5 @@ -46,23 +46,12 @@ typedef enum { BC_PURPLE, } BAR_COLOR; -typedef enum { - MUSIC_LOAD_NEVER, - MUSIC_LOAD_NON_AMBIENT, - MUSIC_LOAD_ALWAYS, -} MUSIC_LOAD_CONDITION; - typedef enum { TLM_FULL, TLM_SEMI, TLM_NONE, } TARGET_LOCK_MODE; -typedef enum { - UI_STYLE_PS1, - UI_STYLE_PC, -} UI_STYLE; - typedef enum { SDM_MINIMAL, SDM_DETAILED, @@ -99,11 +88,16 @@ typedef struct { bool fix_animated_sprites; bool fix_texture_issues; bool enable_ps1_crystals; + + RGB_888 water_color; + int32_t fog_start; + int32_t fog_end; } visuals; struct { bool enable_game_ui; bool enable_photo_mode_ui; + bool enable_wraparound; double text_scale; double bar_scale; UI_STYLE menu_style; @@ -147,13 +141,13 @@ typedef struct { bool enable_cheats; bool enable_console; bool enable_fmv; + bool enable_legal; bool enable_compass_stats; bool enable_total_stats; bool enable_timer_in_inventory; bool enable_demo; - bool enable_eidos_logo; bool enable_loading_screens; - bool enable_cine; + bool enable_cutscenes; STAT_DETAIL_MODE stat_detail_mode; bool enable_walk_to_items; bool enable_enhanced_saves; @@ -216,11 +210,11 @@ typedef struct { bool enable_vsync; bool enable_fps_counter; float anisotropy_filter; - bool pretty_pixels; SCREENSHOT_FORMAT screenshot_format; } rendering; struct { bool new_game_plus_unlock; + ASSAULT_STATS assault_stats; } profile; } CONFIG; diff --git a/src/libtrx/include/libtrx/config/types_tr2.h b/src/libtrx/include/libtrx/config/types_tr2.h index c9d72f166..ebdfe073b 100644 --- a/src/libtrx/include/libtrx/config/types_tr2.h +++ b/src/libtrx/include/libtrx/config/types_tr2.h @@ -1,12 +1,12 @@ #pragma once +#include "../colors.h" +#include "../game/gym.h" #include "../game/input.h" #include "../game/sound/enum.h" #include "../gfx/common.h" #include "../screenshot.h" -#include - typedef enum { LIGHTING_CONTRAST_LOW, LIGHTING_CONTRAST_MEDIUM, @@ -48,12 +48,18 @@ typedef struct { bool enable_fade_effects; bool enable_exit_fade_effects; bool fix_item_rots; + bool fix_texture_issues; int32_t fov; - bool use_pcx_fov; + bool use_psx_fov; + + RGB_888 water_color; + int32_t fog_start; + int32_t fog_end; } visuals; struct { bool enable_photo_mode_ui; + bool enable_wraparound; double text_scale; double bar_scale; } ui; @@ -63,6 +69,7 @@ typedef struct { int32_t music_volume; bool enable_lara_mic; UNDERWATER_MUSIC_MODE underwater_music_mode; + MUSIC_LOAD_CONDITION music_load_condition; } audio; struct { @@ -76,11 +83,15 @@ typedef struct { bool fix_flare_throw_priority; bool fix_walk_run_jump; bool fix_bear_ai; + bool fix_bridge_collision; bool enable_cheats; bool enable_console; bool enable_fmv; + bool enable_legal; + bool enable_cutscenes; bool enable_auto_item_selection; int32_t turbo_speed; + bool enable_game_modes; } gameplay; struct { @@ -112,6 +123,7 @@ typedef struct { } rendering; struct { - bool bonus_level_unlock; + bool new_game_plus_unlock; + ASSAULT_STATS assault_stats; } profile; } CONFIG; diff --git a/src/libtrx/include/libtrx/game/anims/common.h b/src/libtrx/include/libtrx/game/anims/common.h index 07e9e7ac8..103ea87ba 100644 --- a/src/libtrx/include/libtrx/game/anims/common.h +++ b/src/libtrx/include/libtrx/game/anims/common.h @@ -2,6 +2,8 @@ #include "./types.h" +#define NO_ANIM (-1) + void Anim_InitialiseAnims(int32_t num_anims); void Anim_InitialiseChanges(int32_t num_changes); void Anim_InitialiseRanges(int32_t num_ranges); diff --git a/src/libtrx/include/libtrx/game/anims/types.h b/src/libtrx/include/libtrx/game/anims/types.h index c80e665ab..1fa49f180 100644 --- a/src/libtrx/include/libtrx/game/anims/types.h +++ b/src/libtrx/include/libtrx/game/anims/types.h @@ -35,9 +35,7 @@ typedef struct { typedef struct { bool matrix_pop; bool matrix_push; - bool rot_x; - bool rot_y; - bool rot_z; + XYZ_BOOL rot; XYZ_32 pos; } ANIM_BONE; diff --git a/src/libtrx/include/libtrx/game/console.h b/src/libtrx/include/libtrx/game/console.h new file mode 100644 index 000000000..c07b4c616 --- /dev/null +++ b/src/libtrx/include/libtrx/game/console.h @@ -0,0 +1,4 @@ +#pragma once + +#include "console/common.h" +#include "console/history.h" diff --git a/src/libtrx/include/libtrx/game/console/common.h b/src/libtrx/include/libtrx/game/console/common.h index b870cb5ff..263291a24 100644 --- a/src/libtrx/include/libtrx/game/console/common.h +++ b/src/libtrx/include/libtrx/game/console/common.h @@ -11,12 +11,9 @@ void Console_Open(void); void Console_Close(void); bool Console_IsOpened(void); -void Console_ScrollLogs(void); -int32_t Console_GetVisibleLogCount(void); -int32_t Console_GetMaxLogCount(void); - void Console_Log(const char *fmt, ...); COMMAND_RESULT Console_Eval(const char *cmdline); +void Console_Control(void); void Console_Draw(void); extern void Console_DrawBackdrop(void); diff --git a/src/libtrx/include/libtrx/game/const.h b/src/libtrx/include/libtrx/game/const.h index 5b82588fd..2928947b5 100644 --- a/src/libtrx/include/libtrx/game/const.h +++ b/src/libtrx/include/libtrx/game/const.h @@ -14,4 +14,5 @@ #define GRAVITY 6 #define FAST_FALL_SPEED 128 -#define MAX_STATIC_OBJECTS 50 +#define MAX_STATIC_OBJECTS_2D 50 +#define MAX_STATIC_OBJECTS_3D 256 diff --git a/src/libtrx/include/libtrx/game/creature/common.h b/src/libtrx/include/libtrx/game/creature/common.h index 6d33383e1..9d9270839 100644 --- a/src/libtrx/include/libtrx/game/creature/common.h +++ b/src/libtrx/include/libtrx/game/creature/common.h @@ -3,19 +3,38 @@ #include "../collision.h" #include "./types.h" +void Creature_Initialise(int16_t item_num); bool Creature_Activate(int16_t item_num); -bool Creature_IsHostile(const ITEM *item); +void Creature_AIInfo(ITEM *item, AI_INFO *info); +bool Creature_EnsureHabitat( + int16_t item_num, int32_t *wh, const HYBRID_INFO *info); +void Creature_Mood(const ITEM *item, const AI_INFO *info, bool violent); -extern void Creature_Initialise(int16_t item_num); -extern void Creature_Collision( - int16_t item_num, ITEM *lara_item, COLL_INFO *coll); -extern void Creature_AIInfo(ITEM *item, AI_INFO *info); -extern void Creature_Mood(const ITEM *item, const AI_INFO *info, bool violent); -extern int16_t Creature_Turn(ITEM *item, int16_t maximum_turn); -extern void Creature_Tilt(ITEM *item, int16_t angle); -extern void Creature_Head(ITEM *item, int16_t required); -extern bool Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt); -extern int16_t Creature_Effect( +int16_t Creature_Turn(ITEM *item, int16_t max_turn); +void Creature_Tilt(ITEM *item, int16_t angle); +void Creature_Head(ITEM *item, int16_t required); +void Creature_Neck(ITEM *item, int16_t required); + +void Creature_Float(int16_t item_num); +void Creature_Underwater(ITEM *item, int32_t depth); + +bool Creature_CanTargetEnemy(const ITEM *item, const AI_INFO *info); +bool Creature_CheckBaddieOverlap(int16_t item_num); +void Creature_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); +bool Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt); + +void Creature_SpecialKill( + ITEM *item, int32_t kill_anim, int32_t kill_state, int32_t lara_kill_state); +void Creature_Die(int16_t item_num, bool explode); +int32_t Creature_Vault( + int16_t item_num, int16_t angle, int32_t vault, int32_t shift); + +bool Creature_AreAlliesHostile(void); +void Creature_SetAlliesHostile(bool enable); +bool Creature_IsHostile(const ITEM *item); +bool Creature_IsAlly(const ITEM *item); + +int16_t Creature_Effect( const ITEM *item, const BITE *bite, int16_t (*spawn)( int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, diff --git a/src/libtrx/include/libtrx/game/creature/const.h b/src/libtrx/include/libtrx/game/creature/const.h index be8287728..c9052d0ff 100644 --- a/src/libtrx/include/libtrx/game/creature/const.h +++ b/src/libtrx/include/libtrx/game/creature/const.h @@ -1,8 +1,17 @@ #pragma once +#include "../../utils.h" + #define FRONT_ARC DEG_90 #define UNIT_SHADOW 256 #define CREATURE_STALK_DIST (3 * WALL_L) // = 3072 #define CREATURE_ESCAPE_DIST (5 * WALL_L) // = 5120 #define CREATURE_TARGET_DIST (4 * WALL_L) // = 4096 + +#define CREATURE_MISS_CHANCE 0x2000 +#if TR_VERSION >= 2 + #define CREATURE_SHOOT_RANGE SQUARE(WALL_L * 8) // = 0x4000000 = 67108864 +#else + #define CREATURE_SHOOT_RANGE SQUARE(WALL_L * 7) // = 51380224 +#endif diff --git a/src/libtrx/include/libtrx/game/creature/types.h b/src/libtrx/include/libtrx/game/creature/types.h index 492d0612d..097b1241c 100644 --- a/src/libtrx/include/libtrx/game/creature/types.h +++ b/src/libtrx/include/libtrx/game/creature/types.h @@ -1,7 +1,7 @@ #pragma once #include "../items.h" -#include "../pathing.h" +#include "../pathing/types.h" #include "./enum.h" typedef struct { @@ -13,9 +13,7 @@ typedef struct { MOOD_TYPE mood; LOT_INFO lot; XYZ_32 target; -#if TR_VERSION == 2 ITEM *enemy; -#endif } CREATURE; typedef struct { @@ -33,13 +31,19 @@ typedef struct { } AI_INFO; typedef struct { - // TODO: merge -#if TR_VERSION == 1 - int32_t x; - int32_t y; - int32_t z; -#else XYZ_32 pos; -#endif int32_t mesh_num; } BITE; + +typedef struct { + struct { + GAME_OBJECT_ID id; + int16_t active_anim; + int16_t death_anim; + int16_t death_state; + } land; + struct { + GAME_OBJECT_ID id; + int16_t active_anim; + } water; +} HYBRID_INFO; diff --git a/src/libtrx/include/libtrx/game/effects/const.h b/src/libtrx/include/libtrx/game/effects/const.h index 5abc401fd..a5501b2be 100644 --- a/src/libtrx/include/libtrx/game/effects/const.h +++ b/src/libtrx/include/libtrx/game/effects/const.h @@ -1,3 +1,4 @@ #pragma once #define NO_EFFECT (-1) +#define MAX_EFFECTS 1000 diff --git a/src/libtrx/include/libtrx/game/enum_map.def b/src/libtrx/include/libtrx/game/enum_map.def index 963811bde..2811c315f 100644 --- a/src/libtrx/include/libtrx/game/enum_map.def +++ b/src/libtrx/include/libtrx/game/enum_map.def @@ -50,7 +50,10 @@ ENUM_MAP_DEFINE(GAME_BUFFER, GBUF_CREATURE_DATA, "Creature data") ENUM_MAP_DEFINE(GAME_BUFFER, GBUF_CREATURE_LOT, "Creature pathfinding") ENUM_MAP_DEFINE(GAME_BUFFER, GBUF_SAMPLE_INFOS, "Sample information") ENUM_MAP_DEFINE(GAME_BUFFER, GBUF_SAMPLES, "Samples") -ENUM_MAP_DEFINE(GAME_BUFFER, GBUF_VERTEX_BUFFER, "Vertex buffer") + +ENUM_MAP_DEFINE(MUSIC_LOAD_CONDITION, MUSIC_LOAD_NEVER, "never") +ENUM_MAP_DEFINE(MUSIC_LOAD_CONDITION, MUSIC_LOAD_NON_AMBIENT, "non-ambient") +ENUM_MAP_DEFINE(MUSIC_LOAD_CONDITION, MUSIC_LOAD_ALWAYS, "always") ENUM_MAP_DEFINE(UNDERWATER_MUSIC_MODE, UMM_FULL, "full") ENUM_MAP_DEFINE(UNDERWATER_MUSIC_MODE, UMM_QUIET, "quiet") diff --git a/src/libtrx/include/libtrx/game/game.h b/src/libtrx/include/libtrx/game/game.h index 6eaa7234f..81e986a67 100644 --- a/src/libtrx/include/libtrx/game/game.h +++ b/src/libtrx/include/libtrx/game/game.h @@ -1,5 +1,6 @@ #pragma once +#include "./game/enum.h" #include "./game_flow/types.h" void Game_SetIsPlaying(bool is_playing); @@ -11,6 +12,10 @@ void Game_SetCurrentLevel(const GF_LEVEL *level); bool Game_IsInGym(void); bool Game_IsPlayable(void); +GAME_BONUS_FLAG Game_GetBonusFlag(void); +void Game_SetBonusFlag(GAME_BONUS_FLAG flag); +bool Game_IsBonusFlagSet(GAME_BONUS_FLAG flag); + extern bool Game_Start(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); extern void Game_End(void); extern void Game_Suspend(void); diff --git a/src/libtrx/include/libtrx/game/game/enum.h b/src/libtrx/include/libtrx/game/game/enum.h new file mode 100644 index 000000000..b47a83286 --- /dev/null +++ b/src/libtrx/include/libtrx/game/game/enum.h @@ -0,0 +1,9 @@ +#pragma once + +typedef enum { + // clang-format off + GBF_NONE = 0, + GBF_NGPLUS = 1 << 0, + GBF_JAPANESE = 1 << 1, + // clang=format on +} GAME_BONUS_FLAG; diff --git a/src/libtrx/include/libtrx/game/game_buf.h b/src/libtrx/include/libtrx/game/game_buf.h index 38b0b3f55..8cc9b14af 100644 --- a/src/libtrx/include/libtrx/game/game_buf.h +++ b/src/libtrx/include/libtrx/game/game_buf.h @@ -47,7 +47,6 @@ typedef enum { GBUF_SAMPLE_INFOS, GBUF_SAMPLES, GBUF_DEMO_BUFFER, - GBUF_VERTEX_BUFFER, GBUF_NUM_MALLOC_TYPES, // clang-format on } GAME_BUFFER; diff --git a/src/libtrx/include/libtrx/game/game_flow/common.h b/src/libtrx/include/libtrx/game/game_flow/common.h index 3077dc803..d63a1302b 100644 --- a/src/libtrx/include/libtrx/game/game_flow/common.h +++ b/src/libtrx/include/libtrx/game/game_flow/common.h @@ -22,3 +22,7 @@ const GF_LEVEL *GF_GetLevelBefore(const GF_LEVEL *level); void GF_SetCurrentLevel(const GF_LEVEL *level); void GF_SetLevelTitle(GF_LEVEL *level, const char *title); + +// Returns true if any story cutscenes or FMVs occur before gameplay in any +// main level up to the level in the specified save slot. +bool GF_HasAvailableStory(int32_t slot_num); diff --git a/src/libtrx/include/libtrx/game/game_flow/types.h b/src/libtrx/include/libtrx/game/game_flow/types.h index 1524ab571..cb4412b1b 100644 --- a/src/libtrx/include/libtrx/game/game_flow/types.h +++ b/src/libtrx/include/libtrx/game/game_flow/types.h @@ -29,6 +29,7 @@ typedef struct { typedef struct { char *path; + bool is_legal; float display_time; float fade_in_time; float fade_out_time; @@ -75,14 +76,19 @@ typedef struct { typedef struct { const char *path; + bool is_legal; } GF_FMV; typedef struct { -#if TR_VERSION == 1 - RGB_F water_color; - float draw_distance_fade; - float draw_distance_max; -#elif TR_VERSION == 2 + struct { + bool is_present; + float value; + } fog_start, fog_end; + struct { + bool is_present; + RGB_888 value; + } water_color; +#if TR_VERSION == 2 char *sfx_path; #endif } GF_LEVEL_SETTINGS; @@ -142,13 +148,13 @@ typedef struct { GF_FMV *fmvs; }; -#if TR_VERSION == 1 // savegame settings struct { char *savegame_fmt_legacy; char *savegame_fmt_bson; }; +#if TR_VERSION == 1 // global settings struct { float demo_delay; @@ -168,11 +174,6 @@ typedef struct { GF_COMMAND cmd_demo_end; }; - // savegame settings - struct { - char *savegame_fmt_legacy; - }; - // global settings struct { float demo_delay; diff --git a/src/libtrx/include/libtrx/game/game_string.def b/src/libtrx/include/libtrx/game/game_string.def index c6fc6f3ff..8eecf1491 100644 --- a/src/libtrx/include/libtrx/game/game_string.def +++ b/src/libtrx/include/libtrx/game/game_string.def @@ -61,13 +61,13 @@ GS_DEFINE(OSD_AMBIGUOUS_INPUT_2, "Ambiguous input: %s and %s") GS_DEFINE(OSD_AMBIGUOUS_INPUT_3, "Ambiguous input: %s, %s, ...") GS_DEFINE(OSD_UI_ON, "UI enabled") GS_DEFINE(OSD_UI_OFF, "UI disabled") -GS_DEFINE(CONTROL_DEFAULT_KEYS, "Default Keys") -GS_DEFINE(CONTROL_CUSTOM_1, "User Keys 1") -GS_DEFINE(CONTROL_CUSTOM_2, "User Keys 2") -GS_DEFINE(CONTROL_CUSTOM_3, "User Keys 3") -GS_DEFINE(CONTROL_BACKEND_KEYBOARD, "Keyboard") -GS_DEFINE(CONTROL_BACKEND_CONTROLLER, "Controller") -GS_DEFINE(CONTROL_CUSTOMIZE, "Customize Controls") +GS_DEFINE(CONTROLS_DEFAULT_KEYS, "Default Keys") +GS_DEFINE(CONTROLS_CUSTOM_1, "User Keys 1") +GS_DEFINE(CONTROLS_CUSTOM_2, "User Keys 2") +GS_DEFINE(CONTROLS_CUSTOM_3, "User Keys 3") +GS_DEFINE(CONTROLS_BACKEND_KEYBOARD, "Keyboard") +GS_DEFINE(CONTROLS_BACKEND_CONTROLLER, "Controller") +GS_DEFINE(CONTROLS_CUSTOMIZE, "Customize Controls") GS_DEFINE(KEYMAP_RUN, "Run") GS_DEFINE(KEYMAP_BACK, "Back") GS_DEFINE(KEYMAP_LEFT, "Left") @@ -124,6 +124,31 @@ GS_DEFINE(PASSPORT_SAVE_GAME, "Save Game") GS_DEFINE(PASSPORT_NEW_GAME, "New Game") GS_DEFINE(PASSPORT_EXIT_GAME, "Exit Game") GS_DEFINE(PASSPORT_EXIT_TO_TITLE, "Exit to Title") +GS_DEFINE(PASSPORT_SELECT_MODE, "Select Mode") +GS_DEFINE(PASSPORT_MODE_NEW_GAME, "New Game") +GS_DEFINE(PASSPORT_MODE_NEW_GAME_PLUS, "New Game+") +GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP, "Japanese NG") +GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP_PLUS, "Japanese NG+") +GS_DEFINE(PASSPORT_STORY_SO_FAR, "Story so far...") +GS_DEFINE(PASSPORT_LEGACY_SELECT_LEVEL_1, "Legacy saves do not") +GS_DEFINE(PASSPORT_LEGACY_SELECT_LEVEL_2, "support this feature.") GS_DEFINE(SOUND_SET_VOLUMES, "Set Volumes") GS_DEFINE(OSD_TRAPEZOID_FILTER_ON, "Trapezoid filter enabled") GS_DEFINE(OSD_TRAPEZOID_FILTER_OFF, "Trapezoid filter disabled") +GS_DEFINE(DETAIL_INTEGER_FMT, "%d") +GS_DEFINE(DETAIL_FLOAT_FMT, "%.1f") +GS_DEFINE(DETAIL_TITLE, "Graphic Options") +GS_DEFINE(DETAIL_FPS, "FPS") +GS_DEFINE(DETAIL_FOG_START, "Fog start") +GS_DEFINE(DETAIL_FOG_END, "Fog end") +GS_DEFINE(DETAIL_WATER_COLOR_R, "Water color (R)") +GS_DEFINE(DETAIL_WATER_COLOR_G, "Water color (G)") +GS_DEFINE(DETAIL_WATER_COLOR_B, "Water color (B)") +GS_DEFINE(DETAIL_TEXTURE_FILTER, "Texture filter") +GS_DEFINE(DETAIL_BILINEAR, "Bilinear") +GS_DEFINE(DETAIL_TRAPEZOID_FILTER, "Trapezoid filter") +GS_DEFINE(DETAIL_RENDER_MODE, "Render mode") +GS_DEFINE(DETAIL_UI_TEXT_SCALE, "UI text scale") +GS_DEFINE(DETAIL_UI_BAR_SCALE, "UI bar scale") +GS_DEFINE(PAGINATION_NAV, "%d / %d") +GS_DEFINE(MISC_EMPTY_SLOT_FMT, "- EMPTY SLOT -") diff --git a/src/libtrx/include/libtrx/game/gun.h b/src/libtrx/include/libtrx/game/gun.h index a9b31b98c..6cca02787 100644 --- a/src/libtrx/include/libtrx/game/gun.h +++ b/src/libtrx/include/libtrx/game/gun.h @@ -1,3 +1,5 @@ #pragma once +#include "./gun/const.h" + void Gun_AddDynamicLight(void); diff --git a/src/libtrx/include/libtrx/game/gun/const.h b/src/libtrx/include/libtrx/game/gun/const.h new file mode 100644 index 000000000..ca4340222 --- /dev/null +++ b/src/libtrx/include/libtrx/game/gun/const.h @@ -0,0 +1,39 @@ +#pragma once + +#define SHOTGUN_PELLET_SCATTER (DEG_1 * 20) // = 3640 + +#define GUN_AMMO_CLIP 16 +#define GUN_AMMO_QTY (GUN_AMMO_CLIP * 2) // = 32 + +#define SHOTGUN_SHELL_COUNT 2 +#define SHOTGUN_AMMO_CLIP 6 +#define SHOTGUN_AMMO_QTY (SHOTGUN_AMMO_CLIP * SHOTGUN_SHELL_COUNT) // = 12 + +#if TR_VERSION == 1 + #define MAGNUM_AMMO_CLIP 25 + #define MAGNUM_AMMO_QTY (MAGNUM_AMMO_CLIP * 2) // = 50 + + #define UZI_AMMO_CLIP 50 + #define UZI_AMMO_QTY (UZI_AMMO_CLIP * 2) // = 100 +#elif TR_VERSION == 2 + #define MAGNUM_AMMO_CLIP 20 + #define MAGNUM_AMMO_QTY (MAGNUM_AMMO_CLIP * 2) // = 40 + + #define UZI_AMMO_CLIP 40 + #define UZI_AMMO_QTY (UZI_AMMO_CLIP * 2) // = 80 + + #define M16_AMMO_CLIP 40 + #define M16_AMMO_QTY M16_AMMO_CLIP // = 40 + + #define HARPOON_AMMO_CLIP 3 + #define HARPOON_AMMO_QTY HARPOON_AMMO_CLIP // = 3 + #define HARPOON_BOLT_SPEED 150 + #define HARPOON_RECOIL 4 + + #define GRENADE_AMMO_CLIP 1 + #define GRENADE_AMMO_QTY (GRENADE_AMMO_CLIP * 2) // = 2 + #define GRENADE_SPEED 200 + + #define FLARE_AMMO_QTY 6 + #define FLARE_AMMO_JAPANESE_QTY 8 +#endif diff --git a/src/tr2/game/gym.h b/src/libtrx/include/libtrx/game/gym.h similarity index 63% rename from src/tr2/game/gym.h rename to src/libtrx/include/libtrx/game/gym.h index 87e77c423..2a6140693 100644 --- a/src/tr2/game/gym.h +++ b/src/libtrx/include/libtrx/game/gym.h @@ -1,22 +1,18 @@ #pragma once +#include "../config/types.h" + #include -#define MAX_ASSAULT_TIMES 10 - -typedef struct { - uint32_t best_time[MAX_ASSAULT_TIMES]; - uint32_t best_finish[MAX_ASSAULT_TIMES]; - uint32_t finish_count; -} ASSAULT_STATS; - -bool Gym_IsAccessible(void); +extern bool Gym_IsAccessible(void); void Gym_SetInventoryOpenEnabled(bool enabled); bool Gym_IsInventoryOpenEnabled(void); +bool Gym_HasAssaultStats(void); bool Gym_IsAssaultTimerDisplay(void); bool Gym_IsAssaultTimerActive(void); ASSAULT_STATS Gym_GetAssaultStats(void); +void Gym_SetAssaultStats(ASSAULT_STATS stats); void Gym_ResetAssault(void); void Gym_StartAssault(void); diff --git a/src/libtrx/include/libtrx/game/inject/enum.h b/src/libtrx/include/libtrx/game/inject/enum.h index 7c6ee589f..5590d0aca 100644 --- a/src/libtrx/include/libtrx/game/inject/enum.h +++ b/src/libtrx/include/libtrx/game/inject/enum.h @@ -66,7 +66,8 @@ typedef enum { IDT_CAMERA_EDITS = 24, IDT_FRAME_EDITS = 25, IDT_OBJECT_3D_EDITS = 26, - IDT_NUMBER_OF = 27, + IDT_ANIM_CMD_EDITS = 27, + IDT_NUMBER_OF = 28, } INJECTION_DATA_TYPE; typedef enum { diff --git a/src/libtrx/include/libtrx/game/input/common.h b/src/libtrx/include/libtrx/game/input/common.h index 9e1ec9b8a..01ad55caa 100644 --- a/src/libtrx/include/libtrx/game/input/common.h +++ b/src/libtrx/include/libtrx/game/input/common.h @@ -101,3 +101,5 @@ bool Input_AssignToJSONObject( INPUT_ROLE role); INPUT_STATE Input_GetDebounced(const INPUT_STATE input); + +extern const char *Input_GetRoleName(INPUT_ROLE role); diff --git a/src/libtrx/include/libtrx/game/inventory.h b/src/libtrx/include/libtrx/game/inventory.h index 06e21eb48..1d8a42c8a 100644 --- a/src/libtrx/include/libtrx/game/inventory.h +++ b/src/libtrx/include/libtrx/game/inventory.h @@ -6,6 +6,8 @@ #include +extern INVENTORY_MODE g_Inv_Mode; + bool Inv_AddItemNTimes(GAME_OBJECT_ID obj_id, int32_t qty); void Inv_AddAmmo(AMMO_INFO *weapon_ammo, int32_t qty); GAME_OBJECT_ID Inv_GetItemOption(GAME_OBJECT_ID obj_id); diff --git a/src/libtrx/include/libtrx/game/items/common.h b/src/libtrx/include/libtrx/game/items/common.h index fc1f45921..20cbad1d4 100644 --- a/src/libtrx/include/libtrx/game/items/common.h +++ b/src/libtrx/include/libtrx/game/items/common.h @@ -50,3 +50,8 @@ void Item_Translate(ITEM *item, int32_t x, int32_t y, int32_t z); void Item_Animate(ITEM *item); void Item_PlayAnimSFX(const ITEM *item, const ANIM_COMMAND_EFFECT_DATA *data); + +bool Item_TestBoundsCollide( + const ITEM *src_item, const ITEM *dst_item, int32_t radius); +extern const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item); +bool Item_IsTriggerActive(ITEM *item); diff --git a/src/libtrx/include/libtrx/game/items/const.h b/src/libtrx/include/libtrx/game/items/const.h index fb1431048..20c0c2978 100644 --- a/src/libtrx/include/libtrx/game/items/const.h +++ b/src/libtrx/include/libtrx/game/items/const.h @@ -1,8 +1,4 @@ #pragma once #define NO_ITEM (-1) -#if TR_VERSION == 1 - #define MAX_ITEMS 10240 -#else - #define MAX_ITEMS 256 -#endif +#define MAX_ITEMS 10240 diff --git a/src/libtrx/include/libtrx/game/lara/cheat.h b/src/libtrx/include/libtrx/game/lara/cheat.h index 4973cf864..a1f31ab29 100644 --- a/src/libtrx/include/libtrx/game/lara/cheat.h +++ b/src/libtrx/include/libtrx/game/lara/cheat.h @@ -9,4 +9,5 @@ extern bool Lara_Cheat_EnterFlyMode(void); extern bool Lara_Cheat_ExitFlyMode(void); extern bool Lara_Cheat_KillEnemy(int16_t item_num); extern void Lara_Cheat_EndLevel(void); -extern bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z); +extern bool Lara_Cheat_Teleport( + int32_t x, int32_t y, int32_t z, int16_t room_num); diff --git a/src/libtrx/include/libtrx/game/lara/common.h b/src/libtrx/include/libtrx/game/lara/common.h index a67068ec2..068634600 100644 --- a/src/libtrx/include/libtrx/game/lara/common.h +++ b/src/libtrx/include/libtrx/game/lara/common.h @@ -1,5 +1,6 @@ #pragma once +#include "../collision.h" #include "../items.h" #include "types.h" @@ -11,3 +12,6 @@ OBJECT_MESH *Lara_GetMesh(LARA_MESH mesh); void Lara_SetMesh(LARA_MESH mesh, OBJECT_MESH *mesh_ptr); const ANIM_FRAME *Lara_GetHitFrame(const ITEM *item); void Lara_TakeDamage(int16_t damage, bool hit_status); + +bool Lara_TestBoundsCollide(const ITEM *item, int32_t radius); +void Lara_Push(const ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push); diff --git a/src/libtrx/include/libtrx/game/lara/misc.h b/src/libtrx/include/libtrx/game/lara/misc.h index 16dc3b960..051374e7c 100644 --- a/src/libtrx/include/libtrx/game/lara/misc.h +++ b/src/libtrx/include/libtrx/game/lara/misc.h @@ -7,3 +7,4 @@ void Lara_Extinguish(void); int16_t Lara_FloorFront(const ITEM *item, int16_t ang, int32_t dist); void Lara_GetCollisionInfo(const ITEM *item, COLL_INFO *coll); +extern void Lara_CatchFire(void); diff --git a/src/libtrx/include/libtrx/game/lara/types.h b/src/libtrx/include/libtrx/game/lara/types.h index d61f24ab5..581386dd0 100644 --- a/src/libtrx/include/libtrx/game/lara/types.h +++ b/src/libtrx/include/libtrx/game/lara/types.h @@ -59,10 +59,10 @@ typedef struct { XYZ_16 torso_rot; LARA_ARM left_arm; LARA_ARM right_arm; - AMMO_INFO pistols; - AMMO_INFO magnums; - AMMO_INFO uzis; - AMMO_INFO shotgun; + AMMO_INFO pistol_ammo; + AMMO_INFO magnum_ammo; + AMMO_INFO uzi_ammo; + AMMO_INFO shotgun_ammo; LOT_INFO lot; struct { int32_t item_num; diff --git a/src/libtrx/include/libtrx/game/level.h b/src/libtrx/include/libtrx/game/level.h index 9173213d0..6d1109cab 100644 --- a/src/libtrx/include/libtrx/game/level.h +++ b/src/libtrx/include/libtrx/game/level.h @@ -1,4 +1,5 @@ #pragma once #include "level/common.h" +#include "level/settings.h" #include "level/types.h" diff --git a/src/libtrx/include/libtrx/game/level/settings.h b/src/libtrx/include/libtrx/game/level/settings.h new file mode 100644 index 000000000..bd2f424d4 --- /dev/null +++ b/src/libtrx/include/libtrx/game/level/settings.h @@ -0,0 +1,7 @@ +#pragma once + +#include "../../colors.h" + +extern RGB_888 Level_GetWaterColor(void); +extern float Level_GetFogStart(void); +extern float Level_GetFogEnd(void); diff --git a/src/libtrx/include/libtrx/game/los.h b/src/libtrx/include/libtrx/game/los.h new file mode 100644 index 000000000..91b83d02d --- /dev/null +++ b/src/libtrx/include/libtrx/game/los.h @@ -0,0 +1,5 @@ +#pragma once + +#include "./types.h" + +bool LOS_Check(const GAME_VECTOR *start, GAME_VECTOR *target); diff --git a/src/libtrx/include/libtrx/game/math/types.h b/src/libtrx/include/libtrx/game/math/types.h index 29a479678..3374c04d1 100644 --- a/src/libtrx/include/libtrx/game/math/types.h +++ b/src/libtrx/include/libtrx/game/math/types.h @@ -26,6 +26,12 @@ typedef struct { int16_t z; } XYZ_16; +typedef struct { + bool x; + bool y; + bool z; +} XYZ_BOOL; + typedef struct { float x, y, z; } XYZ_F; diff --git a/src/libtrx/include/libtrx/game/objects/common.h b/src/libtrx/include/libtrx/game/objects/common.h index ceeee3db0..f65578f14 100644 --- a/src/libtrx/include/libtrx/game/objects/common.h +++ b/src/libtrx/include/libtrx/game/objects/common.h @@ -7,6 +7,7 @@ #include "ids.h" #include "types.h" +void Object_Reset(void); OBJECT *Object_Get(GAME_OBJECT_ID obj_id); STATIC_OBJECT_3D *Object_Get3DStatic(int32_t static_id); STATIC_OBJECT_2D *Object_Get2DStatic(int32_t static_id); @@ -24,6 +25,7 @@ void Object_StoreMesh(OBJECT_MESH *mesh); int32_t Object_GetMeshCount(void); OBJECT_MESH *Object_FindMesh(int32_t data_offset); +int32_t Object_GetMeshIndex(const OBJECT_MESH *mesh); int32_t Object_GetMeshOffset(const OBJECT_MESH *mesh); void Object_SetMeshOffset(OBJECT_MESH *mesh, int32_t data_offset); @@ -40,6 +42,9 @@ void Object_DrawInterpolatedObject( const OBJECT *obj, uint32_t meshes, const int16_t *extra_rotation, const ANIM_FRAME *frame1, const ANIM_FRAME *frame2, int32_t frac, int32_t rate); +void Object_ApplyExtraRotation( + const int16_t **extra_rotation, const XYZ_BOOL rot_flags, + bool interpolated); #define REGISTER_OBJECT(object_id, setup_func_) \ __attribute__((constructor)) static void M_RegisterObject##object_id(void) \ diff --git a/src/tr1/game/objects/general/bridge_common.h b/src/libtrx/include/libtrx/game/objects/general/bridge_common.h similarity index 88% rename from src/tr1/game/objects/general/bridge_common.h rename to src/libtrx/include/libtrx/game/objects/general/bridge_common.h index 6952e70b7..880dea9ac 100644 --- a/src/tr1/game/objects/general/bridge_common.h +++ b/src/libtrx/include/libtrx/game/objects/general/bridge_common.h @@ -1,6 +1,6 @@ #pragma once -#include "global/types.h" +#include "../../items.h" bool Bridge_IsSameSector(int32_t x, int32_t z, const ITEM *item); int32_t Bridge_GetOffset(const ITEM *item, int32_t x, int32_t y, int32_t z); diff --git a/src/libtrx/include/libtrx/game/objects/general/door.h b/src/libtrx/include/libtrx/game/objects/general/door.h new file mode 100644 index 000000000..845c171cd --- /dev/null +++ b/src/libtrx/include/libtrx/game/objects/general/door.h @@ -0,0 +1,10 @@ +#pragma once + +#include "../../collision.h" + +typedef enum { + DOOR_STATE_CLOSED = 0, + DOOR_STATE_OPEN = 1, +} DOOR_STATE; + +extern void Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); diff --git a/src/libtrx/include/libtrx/game/objects/ids_tr1.def b/src/libtrx/include/libtrx/game/objects/ids_tr1.def index 4186e2d56..32f07eff2 100644 --- a/src/libtrx/include/libtrx/game/objects/ids_tr1.def +++ b/src/libtrx/include/libtrx/game/objects/ids_tr1.def @@ -63,9 +63,9 @@ OBJ_ID_DEFINE(O_DOOR_TYPE_5, 61) OBJ_ID_DEFINE(O_DOOR_TYPE_6, 62) OBJ_ID_DEFINE(O_DOOR_TYPE_7, 63) OBJ_ID_DEFINE(O_DOOR_TYPE_8, 64) -OBJ_ID_DEFINE(O_TRAPDOOR_1, 65) -OBJ_ID_DEFINE(O_TRAPDOOR_2, 66) -OBJ_ID_DEFINE(O_BIGTRAPDOOR, 67) +OBJ_ID_DEFINE(O_TRAPDOOR_TYPE_1, 65) +OBJ_ID_DEFINE(O_TRAPDOOR_TYPE_2, 66) +OBJ_ID_DEFINE(O_TRAPDOOR_TYPE_3, 67) OBJ_ID_DEFINE(O_BRIDGE_FLAT, 68) OBJ_ID_DEFINE(O_BRIDGE_TILT_1, 69) OBJ_ID_DEFINE(O_BRIDGE_TILT_2, 70) @@ -87,12 +87,12 @@ OBJ_ID_DEFINE(O_SHOTGUN_ITEM, 85) OBJ_ID_DEFINE(O_MAGNUM_ITEM, 86) OBJ_ID_DEFINE(O_UZI_ITEM, 87) OBJ_ID_DEFINE(O_PISTOL_AMMO_ITEM, 88) -OBJ_ID_DEFINE(O_SG_AMMO_ITEM, 89) -OBJ_ID_DEFINE(O_MAG_AMMO_ITEM, 90) +OBJ_ID_DEFINE(O_SHOTGUN_AMMO_ITEM, 89) +OBJ_ID_DEFINE(O_MAGNUM_AMMO_ITEM, 90) OBJ_ID_DEFINE(O_UZI_AMMO_ITEM, 91) OBJ_ID_DEFINE(O_EXPLOSIVE_ITEM, 92) -OBJ_ID_DEFINE(O_MEDI_ITEM, 93) -OBJ_ID_DEFINE(O_BIGMEDI_ITEM, 94) +OBJ_ID_DEFINE(O_SMALL_MEDIPACK_ITEM, 93) +OBJ_ID_DEFINE(O_LARGE_MEDIPACK_ITEM, 94) OBJ_ID_DEFINE(O_DETAIL_OPTION, 95) OBJ_ID_DEFINE(O_SOUND_OPTION, 96) OBJ_ID_DEFINE(O_CONTROL_OPTION, 97) @@ -102,12 +102,12 @@ OBJ_ID_DEFINE(O_SHOTGUN_OPTION, 100) OBJ_ID_DEFINE(O_MAGNUM_OPTION, 101) OBJ_ID_DEFINE(O_UZI_OPTION, 102) OBJ_ID_DEFINE(O_PISTOL_AMMO_OPTION, 103) -OBJ_ID_DEFINE(O_SG_AMMO_OPTION, 104) -OBJ_ID_DEFINE(O_MAG_AMMO_OPTION, 105) +OBJ_ID_DEFINE(O_SHOTGUN_AMMO_OPTION, 104) +OBJ_ID_DEFINE(O_MAGNUM_AMMO_OPTION, 105) OBJ_ID_DEFINE(O_UZI_AMMO_OPTION, 106) OBJ_ID_DEFINE(O_EXPLOSIVE_OPTION, 107) -OBJ_ID_DEFINE(O_MEDI_OPTION, 108) -OBJ_ID_DEFINE(O_BIGMEDI_OPTION, 109) +OBJ_ID_DEFINE(O_SMALL_MEDIPACK_OPTION, 108) +OBJ_ID_DEFINE(O_LARGE_MEDIPACK_OPTION, 109) OBJ_ID_DEFINE(O_PUZZLE_ITEM_1, 110) OBJ_ID_DEFINE(O_PUZZLE_ITEM_2, 111) OBJ_ID_DEFINE(O_PUZZLE_ITEM_3, 112) diff --git a/src/libtrx/include/libtrx/game/objects/names_tr1.def b/src/libtrx/include/libtrx/game/objects/names_tr1.def index 786cd737f..4d9edc8d1 100644 --- a/src/libtrx/include/libtrx/game/objects/names_tr1.def +++ b/src/libtrx/include/libtrx/game/objects/names_tr1.def @@ -1,13 +1,13 @@ // pickups on the ground -OBJ_NAME_DEFINE(O_MEDI_ITEM, "small_medipack", "Small Medi Pack") -OBJ_NAME_DEFINE(O_BIGMEDI_ITEM, "large_medipack", "Large Medi Pack") +OBJ_NAME_DEFINE(O_SMALL_MEDIPACK_ITEM, "small_medipack", "Small Medi Pack") +OBJ_NAME_DEFINE(O_LARGE_MEDIPACK_ITEM, "large_medipack", "Large Medi Pack") OBJ_NAME_DEFINE(O_PISTOL_ITEM, "pistols", "Pistols") OBJ_NAME_DEFINE(O_SHOTGUN_ITEM, "shotgun", "Shotgun") OBJ_NAME_DEFINE(O_MAGNUM_ITEM, "magnums", "Magnums") OBJ_NAME_DEFINE(O_UZI_ITEM, "uzis", "Uzis") OBJ_NAME_DEFINE(O_PISTOL_AMMO_ITEM, "pistol_ammo", "Pistol Clips") -OBJ_NAME_DEFINE(O_SG_AMMO_ITEM, "shotgun_ammo", "Shotgun Shells") -OBJ_NAME_DEFINE(O_MAG_AMMO_ITEM, "magnums_ammo", "Magnum Clips") +OBJ_NAME_DEFINE(O_SHOTGUN_AMMO_ITEM, "shotgun_ammo", "Shotgun Shells") +OBJ_NAME_DEFINE(O_MAGNUM_AMMO_ITEM, "magnums_ammo", "Magnum Clips") OBJ_NAME_DEFINE(O_UZI_AMMO_ITEM, "uzis_ammo", "Uzi Clips") OBJ_NAME_DEFINE(O_EXPLOSIVE_ITEM, "grenade", "Grenade") OBJ_NAME_DEFINE(O_KEY_ITEM_1, "key_1", "Key 1") @@ -36,30 +36,30 @@ OBJ_NAME_DEFINE(O_SOUND_OPTION, "sound", "Sound") OBJ_NAME_DEFINE(O_GAMMA_OPTION, "gamma", "Gamma") OBJ_NAME_DEFINE(O_MAP_CLOSED, "map", "Map") -OBJ_ALIAS_DEFINE(O_PISTOL_OPTION, O_PISTOL_ITEM) -OBJ_ALIAS_DEFINE(O_PISTOL_AMMO_OPTION, O_PISTOL_ITEM) -OBJ_ALIAS_DEFINE(O_SHOTGUN_OPTION, O_SHOTGUN_ITEM) -OBJ_ALIAS_DEFINE(O_MAGNUM_OPTION, O_MAGNUM_ITEM) -OBJ_ALIAS_DEFINE(O_UZI_OPTION, O_UZI_ITEM) -OBJ_ALIAS_DEFINE(O_SG_AMMO_OPTION, O_SG_AMMO_ITEM) -OBJ_ALIAS_DEFINE(O_MAG_AMMO_OPTION, O_MAG_AMMO_ITEM) -OBJ_ALIAS_DEFINE(O_UZI_AMMO_OPTION, O_UZI_AMMO_ITEM) -OBJ_ALIAS_DEFINE(O_EXPLOSIVE_OPTION, O_EXPLOSIVE_ITEM) -OBJ_ALIAS_DEFINE(O_MEDI_OPTION, O_MEDI_ITEM) -OBJ_ALIAS_DEFINE(O_BIGMEDI_OPTION, O_BIGMEDI_ITEM) -OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_1, O_PUZZLE_ITEM_1) -OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_2, O_PUZZLE_ITEM_2) -OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_3, O_PUZZLE_ITEM_3) -OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_4, O_PUZZLE_ITEM_4) -OBJ_ALIAS_DEFINE(O_KEY_OPTION_1, O_KEY_ITEM_1) -OBJ_ALIAS_DEFINE(O_KEY_OPTION_2, O_KEY_ITEM_2) -OBJ_ALIAS_DEFINE(O_KEY_OPTION_3, O_KEY_ITEM_3) -OBJ_ALIAS_DEFINE(O_KEY_OPTION_4, O_KEY_ITEM_4) -OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_1, O_PICKUP_ITEM_1) -OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_2, O_PICKUP_ITEM_2) -OBJ_ALIAS_DEFINE(O_LEADBAR_OPTION, O_LEADBAR_ITEM) -OBJ_ALIAS_DEFINE(O_SCION_OPTION, O_SCION_ITEM_1) -OBJ_ALIAS_DEFINE(O_PASSPORT_CLOSED, O_PASSPORT_OPTION) +OBJ_ALIAS_DEFINE(O_PISTOL_OPTION, O_PISTOL_ITEM) +OBJ_ALIAS_DEFINE(O_PISTOL_AMMO_OPTION, O_PISTOL_ITEM) +OBJ_ALIAS_DEFINE(O_SHOTGUN_OPTION, O_SHOTGUN_ITEM) +OBJ_ALIAS_DEFINE(O_MAGNUM_OPTION, O_MAGNUM_ITEM) +OBJ_ALIAS_DEFINE(O_UZI_OPTION, O_UZI_ITEM) +OBJ_ALIAS_DEFINE(O_SHOTGUN_AMMO_OPTION, O_SHOTGUN_AMMO_ITEM) +OBJ_ALIAS_DEFINE(O_MAGNUM_AMMO_OPTION, O_MAGNUM_AMMO_ITEM) +OBJ_ALIAS_DEFINE(O_UZI_AMMO_OPTION, O_UZI_AMMO_ITEM) +OBJ_ALIAS_DEFINE(O_EXPLOSIVE_OPTION, O_EXPLOSIVE_ITEM) +OBJ_ALIAS_DEFINE(O_SMALL_MEDIPACK_OPTION, O_SMALL_MEDIPACK_ITEM) +OBJ_ALIAS_DEFINE(O_LARGE_MEDIPACK_OPTION, O_LARGE_MEDIPACK_ITEM) +OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_1, O_PUZZLE_ITEM_1) +OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_2, O_PUZZLE_ITEM_2) +OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_3, O_PUZZLE_ITEM_3) +OBJ_ALIAS_DEFINE(O_PUZZLE_OPTION_4, O_PUZZLE_ITEM_4) +OBJ_ALIAS_DEFINE(O_KEY_OPTION_1, O_KEY_ITEM_1) +OBJ_ALIAS_DEFINE(O_KEY_OPTION_2, O_KEY_ITEM_2) +OBJ_ALIAS_DEFINE(O_KEY_OPTION_3, O_KEY_ITEM_3) +OBJ_ALIAS_DEFINE(O_KEY_OPTION_4, O_KEY_ITEM_4) +OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_1, O_PICKUP_ITEM_1) +OBJ_ALIAS_DEFINE(O_PICKUP_OPTION_2, O_PICKUP_ITEM_2) +OBJ_ALIAS_DEFINE(O_LEADBAR_OPTION, O_LEADBAR_ITEM) +OBJ_ALIAS_DEFINE(O_SCION_OPTION, O_SCION_ITEM_1) +OBJ_ALIAS_DEFINE(O_PASSPORT_CLOSED, O_PASSPORT_OPTION) // general objects OBJ_NAME_DEFINE(O_LARA, "lara", "Lara") @@ -127,9 +127,9 @@ OBJ_NAME_DEFINE(O_DOOR_TYPE_5, "door_5", "Door 5") OBJ_NAME_DEFINE(O_DOOR_TYPE_6, "door_6", "Door 6") OBJ_NAME_DEFINE(O_DOOR_TYPE_7, "door_7", "Door 7") OBJ_NAME_DEFINE(O_DOOR_TYPE_8, "door_8", "Door 8") -OBJ_NAME_DEFINE(O_TRAPDOOR_1, "trapdoor_1", "Trapdoor 1") -OBJ_NAME_DEFINE(O_TRAPDOOR_2, "trapdoor_2", "Trapdoor 2") -OBJ_NAME_DEFINE(O_BIGTRAPDOOR, "big_trapdoor", "Big Trapdoor") +OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_1, "trapdoor_1", "Trapdoor 1") +OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_2, "trapdoor_2", "Trapdoor 2") +OBJ_NAME_DEFINE(O_TRAPDOOR_TYPE_3, "trapdoor_3", "Trapdoor 3") OBJ_NAME_DEFINE(O_BRIDGE_FLAT, "bridge_flat", "Bridge Flat") OBJ_NAME_DEFINE(O_BRIDGE_TILT_1, "bridge_tilt_1", "Bridge Tilt 1") OBJ_NAME_DEFINE(O_BRIDGE_TILT_2, "bridge_tilt_2", "Bridge Tilt 2") diff --git a/src/libtrx/include/libtrx/game/objects/traps/movable_block.h b/src/libtrx/include/libtrx/game/objects/traps/movable_block.h index bdafcfbca..4f1815955 100644 --- a/src/libtrx/include/libtrx/game/objects/traps/movable_block.h +++ b/src/libtrx/include/libtrx/game/objects/traps/movable_block.h @@ -3,5 +3,6 @@ #include "../../rooms.h" void MovableBlock_Initialise(int16_t item_num); +void MovableBlock_UpdateRotation(ITEM *item, int16_t rot_y); void MovableBlock_SetupFloor(void); void MovableBlock_HandleFlipMap(ROOM_FLIP_STATUS flip_status); diff --git a/src/libtrx/include/libtrx/game/objects/types.h b/src/libtrx/include/libtrx/game/objects/types.h index 27d0c555b..88b289dcd 100644 --- a/src/libtrx/include/libtrx/game/objects/types.h +++ b/src/libtrx/include/libtrx/game/objects/types.h @@ -4,7 +4,7 @@ #include "../collision.h" #include "../items/types.h" #include "../rooms/enum.h" -#include "../savegame.h" +#include "../savegame/enum.h" #include "../types.h" #include @@ -33,7 +33,9 @@ typedef struct { FACE3 *tex_face3s; FACE4 *flat_face4s; FACE3 *flat_face3s; + bool enable_reflections; + bool disable_lighting; } OBJECT_MESH; #if TR_VERSION == 1 @@ -76,6 +78,7 @@ typedef struct OBJECT { int16_t shadow_size; int16_t smartness; bool enable_interpolation; + XYZ_BOOL base_rot; union { uint16_t flags; diff --git a/src/libtrx/include/libtrx/game/objects/vars.h b/src/libtrx/include/libtrx/game/objects/vars.h index 88c3d555a..85d5df60c 100644 --- a/src/libtrx/include/libtrx/game/objects/vars.h +++ b/src/libtrx/include/libtrx/game/objects/vars.h @@ -10,6 +10,7 @@ extern const GAME_OBJECT_ID g_PickupObjects[]; extern const GAME_OBJECT_ID g_AnimObjects[]; extern const GAME_OBJECT_ID g_NullObjects[]; extern const GAME_OBJECT_ID g_InvObjects[]; +extern const GAME_OBJECT_ID g_WaterSpriteObjects[]; extern const GAME_OBJECT_PAIR g_ItemToInvObjectMap[]; extern const GAME_OBJECT_PAIR g_KeyItemToReceptacleMap[]; diff --git a/src/libtrx/include/libtrx/game/output.h b/src/libtrx/include/libtrx/game/output.h index a8aea7e30..4322b2d0a 100644 --- a/src/libtrx/include/libtrx/game/output.h +++ b/src/libtrx/include/libtrx/game/output.h @@ -1,6 +1,8 @@ #pragma once +#include "./output/background.h" #include "./output/common.h" #include "./output/const.h" +#include "./output/draw.h" #include "./output/textures.h" #include "./output/types.h" diff --git a/src/libtrx/include/libtrx/game/output/background.h b/src/libtrx/include/libtrx/game/output/background.h new file mode 100644 index 000000000..444896925 --- /dev/null +++ b/src/libtrx/include/libtrx/game/output/background.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../../engine/image.h" + +bool Output_LoadBackgroundFromFile(const char *path); + +extern bool Output_LoadBackgroundFromImage(const IMAGE *image); +extern void Output_LoadBackgroundFromObject(void); +extern void Output_UnloadBackground(void); + +extern void Output_DrawBackground(void); + +// TODO: make these functions private once output module is consolidated +char *Output_GetLastBackgroundPath(void); +void Output_ClearLastBackgroundPath(void); diff --git a/src/libtrx/include/libtrx/game/output/common.h b/src/libtrx/include/libtrx/game/output/common.h index cff22d600..07d985b72 100644 --- a/src/libtrx/include/libtrx/game/output/common.h +++ b/src/libtrx/include/libtrx/game/output/common.h @@ -2,16 +2,15 @@ #include "../rooms.h" +extern void Output_ObserveLevelLoad(void); +extern void Output_ObserveLevelUnload(void); +extern void Output_ObserveRoomFlip(const ROOM *room); + extern bool Output_MakeScreenshot(const char *path); extern void Output_BeginScene(void); extern void Output_EndScene(void); -extern bool Output_LoadBackgroundFromFile(const char *file_name); -extern void Output_LoadBackgroundFromObject(void); -extern void Output_UnloadBackground(void); - extern void Output_DrawBlackRectangle(int32_t opacity); -extern void Output_DrawBackground(void); extern void Output_DrawPolyList(void); extern void Output_SetupBelowWater(bool is_underwater); @@ -36,3 +35,8 @@ void Output_LightRoom(ROOM *room); void Output_ResetDynamicLights(void); void Output_AddDynamicLight(XYZ_32 pos, int32_t intensity, int32_t falloff); + +int32_t Output_GetFogStart(void); +void Output_SetFogStart(int32_t dist); +extern int32_t Output_GetFogEnd(void); +extern void Output_SetFogEnd(int32_t dist); diff --git a/src/libtrx/include/libtrx/game/output/const.h b/src/libtrx/include/libtrx/game/output/const.h index 022460bae..c14ff2e53 100644 --- a/src/libtrx/include/libtrx/game/output/const.h +++ b/src/libtrx/include/libtrx/game/output/const.h @@ -6,4 +6,9 @@ #define TEXTURE_PAGE_HEIGHT 256 #define TEXTURE_PAGE_SIZE (TEXTURE_PAGE_WIDTH * TEXTURE_PAGE_HEIGHT) +#define SHADE_CAUSTICS 0x300 +#define SHADE_MAX 0x1FFF +#define SHADE_LOW 0x1400 +#define SHADE_NEUTRAL 0x1000 + #define WIBBLE_SIZE 32 diff --git a/src/libtrx/include/libtrx/game/output/draw.h b/src/libtrx/include/libtrx/game/output/draw.h new file mode 100644 index 000000000..048bb7bba --- /dev/null +++ b/src/libtrx/include/libtrx/game/output/draw.h @@ -0,0 +1,18 @@ +#pragma once + +#include "../../config/types.h" + +#include + +typedef enum { + TS_HEADING = 0, + TS_BACKGROUND = 1, + TS_REQUESTED = 2, +} TEXT_STYLE; + +extern void Output_DrawTextBackground( + UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t w, int32_t h, int32_t z, + TEXT_STYLE text_style); +extern void Output_DrawTextOutline( + UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t w, int32_t h, int32_t z, + TEXT_STYLE text_style); diff --git a/src/libtrx/include/libtrx/game/output/objects.h b/src/libtrx/include/libtrx/game/output/objects.h new file mode 100644 index 000000000..d32007c2a --- /dev/null +++ b/src/libtrx/include/libtrx/game/output/objects.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../objects/types.h" + +#if TR_VERSION == 1 +extern void Output_Meshes_ObserveObjectMeshSwap( + const OBJECT_MESH *mesh_1, const OBJECT_MESH *mesh_2); +extern void Output_Meshes_ObserveObjectMeshUpdate(const OBJECT_MESH *mesh); +#endif diff --git a/src/libtrx/include/libtrx/game/output/types.h b/src/libtrx/include/libtrx/game/output/types.h index 8ac1e32fc..40fa91bbb 100644 --- a/src/libtrx/include/libtrx/game/output/types.h +++ b/src/libtrx/include/libtrx/game/output/types.h @@ -1,5 +1,7 @@ #pragma once +#include "../../colors.h" + #include typedef enum { @@ -36,6 +38,7 @@ typedef struct { typedef struct { uint16_t draw_type; uint16_t tex_page; + int32_t uv_count; TEXTURE_UV uv[4]; #if TR_VERSION == 2 TEXTURE_UV uv_backup[4]; @@ -59,25 +62,6 @@ typedef struct ANIMATED_TEXTURE_RANGE { struct ANIMATED_TEXTURE_RANGE *next_range; } ANIMATED_TEXTURE_RANGE; -typedef struct { - float r; - float g; - float b; -} RGB_F; - -typedef struct { - uint8_t r; - uint8_t g; - uint8_t b; -} RGB_888; - -typedef struct { - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; -} RGBA_8888; - typedef struct { uint8_t index[256]; } LIGHT_MAP; diff --git a/src/libtrx/include/libtrx/game/pathing/const.h b/src/libtrx/include/libtrx/game/pathing/const.h index 8d39823a3..b2d5661ec 100644 --- a/src/libtrx/include/libtrx/game/pathing/const.h +++ b/src/libtrx/include/libtrx/game/pathing/const.h @@ -4,6 +4,7 @@ #define NO_BOX (-1) #define BOX_ZONE(num) (((num) / STEP_L) - 1) +#define LOT_SLOT_COUNT 32 #if TR_VERSION == 1 #define MAX_ZONES 2 #else diff --git a/src/libtrx/include/libtrx/game/pathing/lot.h b/src/libtrx/include/libtrx/game/pathing/lot.h index 76ea8d7fd..262bdaafa 100644 --- a/src/libtrx/include/libtrx/game/pathing/lot.h +++ b/src/libtrx/include/libtrx/game/pathing/lot.h @@ -1,3 +1,8 @@ #pragma once +#include "../creature/types.h" + extern bool LOT_EnableBaddieAI(int16_t item_num, bool always); +extern void LOT_DisableBaddieAI(int16_t item_num); +extern CREATURE *LOT_GetBaddieSlot(int32_t i); +extern void LOT_ClearLOT(LOT_INFO *LOT); diff --git a/src/libtrx/include/libtrx/game/rooms/common.h b/src/libtrx/include/libtrx/game/rooms/common.h index 094885268..1d24b62b7 100644 --- a/src/libtrx/include/libtrx/game/rooms/common.h +++ b/src/libtrx/include/libtrx/game/rooms/common.h @@ -31,6 +31,7 @@ void Room_PopulateSectorData( int16_t Room_GetIndexFromPos(int32_t x, int32_t y, int32_t z); int32_t Room_FindByPos(int32_t x, int32_t y, int32_t z); +int32_t Room_GetFlippedBaseRoom(int32_t room_num); BOUNDS_32 Room_GetWorldBounds(void); extern SECTOR *Room_GetSector( @@ -45,7 +46,13 @@ void Room_SetAbyssHeight(int16_t height); bool Room_IsAbyssHeight(int16_t height); HEIGHT_TYPE Room_GetHeightType(void); int16_t Room_GetHeight(const SECTOR *sector, int32_t x, int32_t y, int32_t z); +int16_t Room_GetHeightEx( + const SECTOR *sector, int32_t x, int32_t y, int32_t z, bool fix_tilts); +extern int32_t Room_GetWaterHeight( + int32_t x, int32_t y, int32_t z, int16_t room_num); int16_t Room_GetCeiling(const SECTOR *sector, int32_t x, int32_t y, int32_t z); +int16_t Room_GetCeilingEx( + const SECTOR *sector, int32_t x, int32_t y, int32_t z, bool fix_tilts); extern int16_t Room_GetTiltType( const SECTOR *sector, int32_t x, int32_t y, int32_t z); extern int32_t Room_FindGridShift(int32_t src, int32_t dst); diff --git a/src/libtrx/include/libtrx/game/rooms/enum.h b/src/libtrx/include/libtrx/game/rooms/enum.h index 8dcbab259..ad7abda5f 100644 --- a/src/libtrx/include/libtrx/game/rooms/enum.h +++ b/src/libtrx/include/libtrx/game/rooms/enum.h @@ -67,9 +67,7 @@ typedef enum { TT_ANTIPAD = 6, TT_COMBAT = 7, TT_DUMMY = 8, -#if TR_VERSION == 2 TT_ANTITRIGGER = 9, -#endif } TRIGGER_TYPE; #if TR_VERSION == 2 diff --git a/src/libtrx/include/libtrx/game/savegame.h b/src/libtrx/include/libtrx/game/savegame.h index 61f6305ea..ac83fd6bd 100644 --- a/src/libtrx/include/libtrx/game/savegame.h +++ b/src/libtrx/include/libtrx/game/savegame.h @@ -1,25 +1,6 @@ #pragma once -#include - -typedef enum { - SAVEGAME_STAGE_BEFORE_LOAD, - SAVEGAME_STAGE_AFTER_LOAD, - SAVEGAME_STAGE_BEFORE_SAVE, -} SAVEGAME_STAGE; - -// Remembers the slot used when the player starts a loaded game. -// Persists across level reloads. -void Savegame_BindSlot(int32_t slot_num); - -// Removes the binding of the current slot. Used when the player exits to -// title, issues a command like `/play` etc. -void Savegame_UnbindSlot(void); - -// Returns the currently bound slot number. If there is none, returns -1. -int32_t Savegame_GetBoundSlot(void); - -extern int32_t Savegame_GetSlotCount(void); -extern bool Savegame_IsSlotFree(int32_t slot_num); -extern bool Savegame_Load(int32_t slot_num); -extern bool Savegame_Save(int32_t slot_num); +#include "./savegame/common.h" +#include "./savegame/const.h" +#include "./savegame/enum.h" +#include "./savegame/types.h" diff --git a/src/libtrx/include/libtrx/game/savegame/bson.h b/src/libtrx/include/libtrx/game/savegame/bson.h new file mode 100644 index 000000000..1545d85d4 --- /dev/null +++ b/src/libtrx/include/libtrx/game/savegame/bson.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#pragma pack(push, 1) +typedef struct { + uint32_t magic; + int16_t initial_version; + uint16_t version; + int32_t compressed_size; + int32_t uncompressed_size; +} SAVEGAME_BSON_HEADER; + +typedef struct { + uint32_t flags; + int32_t counter; + int32_t level_num; + int32_t title_size; +} SAVEGAME_BSON_EXTENDED_HEADER; +#pragma pack(pop) diff --git a/src/libtrx/include/libtrx/game/savegame/common.h b/src/libtrx/include/libtrx/game/savegame/common.h new file mode 100644 index 000000000..77965f637 --- /dev/null +++ b/src/libtrx/include/libtrx/game/savegame/common.h @@ -0,0 +1,72 @@ +#pragma once + +#include "../game_flow/types.h" +#include "./types.h" + +// Loading a saved game is divided into two phases. First, the game reads the +// savegame file contents to look for the level number. The rest of the save +// data is stored in a special buffer in the g_GameInfo. Then the engine +// continues to execute the normal game flow and loads the specified level. +// Second phase occurs after everything finishes loading, e.g. items, +// creatures, triggers etc., and is what actually sets Lara's health, creatures +// status, triggers, inventory etc. + +void Savegame_RegisterStrategy(SAVEGAME_STRATEGY strategy); +void Savegame_Init(void); +void Savegame_Shutdown(void); +bool Savegame_IsInitialised(void); +void Savegame_ScanSavedGames(void); + +SAVEGAME_VERSION Savegame_GetInitialVersion(void); +void Savegame_SetInitialVersion(SAVEGAME_VERSION version); +int32_t Savegame_GetCounter(void); +int32_t Savegame_GetTotalCount(void); +int32_t Savegame_GetLevelNumber(int32_t slot_num); +bool Savegame_IsSlotFree(int32_t slot_num); +bool Savegame_RestartAvailable(int32_t slot_num); + +// Remembers the slot used when the player starts a loaded game. +// Persists across level reloads. +void Savegame_BindSlot(int32_t slot_num); + +// Removes the binding of the current slot. Used when the player exits to +// title, issues a command like `/play` etc. +void Savegame_UnbindSlot(void); + +// Returns the currently bound slot number. If there is none, returns -1. +int32_t Savegame_GetBoundSlot(void); + +// Returns the most recently created slot number. If there is none, returns -1. +int32_t Savegame_GetMostRecentlyCreatedSlot(void); + +// Returns the most recently created slot number. If there is none, returns -1. +int32_t Savegame_GetMostRecentlyUsedSlot(void); + +void Savegame_ProcessItemsBeforeLoad(void); +void Savegame_ProcessItemsBeforeSave(void); +bool Savegame_Load(int32_t slot_num); +bool Savegame_Save(int32_t slot_num); +bool Savegame_UpdateDeathCounters(int32_t slot_num, int32_t death_count); +bool Savegame_LoadOnlyResumeInfo(int32_t slot_num); + +void Savegame_InitCurrentInfo(void); +void Savegame_SetCurrentInfo(int32_t current_slot, int32_t src_slot); +RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level); +const SAVEGAME_INFO *Savegame_GetSavegameInfo(int32_t slot_num); +void Savegame_ResetCurrentInfo(const GF_LEVEL *level); +void Savegame_CarryCurrentInfoToNextLevel( + const GF_LEVEL *src_level, const GF_LEVEL *dst_level); +void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level); + +void Savegame_SetDefaultStats(const GF_LEVEL *level, STATS_COMMON stats); +STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *level); + +extern int32_t Savegame_GetSlotCount(void); +extern void Savegame_HighlightNewestSlot(void); +extern void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level); + +#define REGISTER_SAVEGAME_STRATEGY(strategy_) \ + __attribute__((__constructor__)) static void M_Register(void) \ + { \ + Savegame_RegisterStrategy(strategy_); \ + } diff --git a/src/libtrx/include/libtrx/game/savegame/const.h b/src/libtrx/include/libtrx/game/savegame/const.h new file mode 100644 index 000000000..a4222ce4e --- /dev/null +++ b/src/libtrx/include/libtrx/game/savegame/const.h @@ -0,0 +1,3 @@ +#pragma once + +#define SAVEGAME_CURRENT_VERSION 8 diff --git a/src/libtrx/include/libtrx/game/savegame/enum.h b/src/libtrx/include/libtrx/game/savegame/enum.h new file mode 100644 index 000000000..a4110eccb --- /dev/null +++ b/src/libtrx/include/libtrx/game/savegame/enum.h @@ -0,0 +1,33 @@ +#pragma once + +typedef enum { + SAVEGAME_STAGE_BEFORE_LOAD, + SAVEGAME_STAGE_AFTER_LOAD, + SAVEGAME_STAGE_BEFORE_SAVE, +} SAVEGAME_STAGE; + +typedef enum { + SAVEGAME_FORMAT_INVALID = 0, + SAVEGAME_FORMAT_LEGACY = 1, + SAVEGAME_FORMAT_BSON = 2, +} SAVEGAME_FORMAT; + +typedef enum { + VERSION_LEGACY = -1, + VERSION_0 = 0, + VERSION_1 = 1, + VERSION_2 = 2, + VERSION_3 = 3, + VERSION_4 = 4, + VERSION_5 = 5, + VERSION_6 = 6, + + // Added extra footer after the compressed BSON structure for quicker + // access to essential data, such as the level counter and the level title, + // without the need to parse the entire BSON document. + // Added TR2+ stats ammo hits/used, health packs used, distance travelled. + VERSION_7 = 7, + + // Added the current TRX version string. + VERSION_8 = 8, +} SAVEGAME_VERSION; diff --git a/src/libtrx/include/libtrx/game/savegame/types.h b/src/libtrx/include/libtrx/game/savegame/types.h new file mode 100644 index 000000000..a74e0db54 --- /dev/null +++ b/src/libtrx/include/libtrx/game/savegame/types.h @@ -0,0 +1,73 @@ +#pragma once + +#include "../../filesystem.h" +#include "../lara/enum.h" +#include "../stats/types.h" + +typedef struct { + LEVEL_STATS stats; + uint8_t small_medipacks; + uint8_t large_medipacks; + uint16_t pistol_ammo; + uint16_t magnum_ammo; + uint16_t uzi_ammo; + uint16_t shotgun_ammo; + LARA_GUN_STATE gun_status; + LARA_GUN_TYPE equipped_gun_type; +#if TR_VERSION == 1 + LARA_GUN_TYPE holsters_gun_type; + LARA_GUN_TYPE back_gun_type; + uint8_t num_scions; + int32_t lara_hitpoints; +#elif TR_VERSION == 2 + uint16_t m16_ammo; + uint16_t grenade_ammo; + uint16_t harpoon_ammo; + uint8_t flares; +#endif + + union { + uint16_t all; + struct { + uint16_t available : 1; + uint16_t has_pistols : 1; + uint16_t has_magnums : 1; + uint16_t has_uzis : 1; + uint16_t has_shotgun : 1; +#if TR_VERSION == 1 + uint16_t costume : 1; + uint16_t pad : 10; +#elif TR_VERSION == 2 + uint16_t has_m16 : 1; + uint16_t has_grenade : 1; + uint16_t has_harpoon : 1; + uint16_t pad : 8; +#endif + }; + } flags; +} RESUME_INFO; + +typedef struct { + SAVEGAME_FORMAT format; + char *full_path; + int32_t counter; + int32_t level_num; + char *level_title; + int16_t initial_version; + struct { + bool restart; + bool select_level; + } features; +} SAVEGAME_INFO; + +typedef struct { + bool allow_load; + bool allow_save; + SAVEGAME_FORMAT format; + const char *(*get_save_file_pattern_func)(void); + bool (*fill_info_func)(MYFILE *fp, SAVEGAME_INFO *info); + bool (*load_from_file_func)(MYFILE *fp); + bool (*load_only_resume_info_func)(MYFILE *fp); + void (*save_to_file_func)(MYFILE *fp, SAVEGAME_INFO *savegame_info); + bool (*update_death_counters_func)(MYFILE *fp, int32_t death_count); +} SAVEGAME_STRATEGY; diff --git a/src/tr2/game/scaler.h b/src/libtrx/include/libtrx/game/scaler.h similarity index 100% rename from src/tr2/game/scaler.h rename to src/libtrx/include/libtrx/game/scaler.h diff --git a/src/libtrx/include/libtrx/game/shell.h b/src/libtrx/include/libtrx/game/shell.h index 99b922576..3128bda38 100644 --- a/src/libtrx/include/libtrx/game/shell.h +++ b/src/libtrx/include/libtrx/game/shell.h @@ -3,6 +3,11 @@ #include #include +typedef struct { + int32_t w; + int32_t h; +} SHELL_SIZE; + extern void Shell_Shutdown(void); extern SDL_Window *Shell_GetWindow(void); @@ -16,9 +21,10 @@ void Shell_GetCommandLine(int *arg_count, const char ***args); void Shell_ScheduleExit(void); bool Shell_IsExiting(void); -int32_t Shell_GetCurrentDisplayWidth(void); -int32_t Shell_GetCurrentDisplayHeight(void); -void Shell_GetWindowSize(int32_t *out_width, int32_t *out_height); +bool Shell_IsFullscreen(void); +SHELL_SIZE Shell_GetWindowSize(void); +SHELL_SIZE Shell_GetCurrentSize(void); +SHELL_SIZE Shell_GetCurrentDisplaySize(void); extern const char *Shell_GetConfigPath(void); extern const char *Shell_GetGameFlowPath(void); diff --git a/src/libtrx/include/libtrx/game/stats.h b/src/libtrx/include/libtrx/game/stats.h index 547864ffb..e0ceb05c4 100644 --- a/src/libtrx/include/libtrx/game/stats.h +++ b/src/libtrx/include/libtrx/game/stats.h @@ -1,3 +1,4 @@ #pragma once #include "stats/common.h" +#include "stats/types.h" diff --git a/src/libtrx/include/libtrx/game/stats/common.h b/src/libtrx/include/libtrx/game/stats/common.h index 045b4d382..0e83bca63 100644 --- a/src/libtrx/include/libtrx/game/stats/common.h +++ b/src/libtrx/include/libtrx/game/stats/common.h @@ -1,3 +1,4 @@ #pragma once +extern void Stats_StartTimer(void); extern void Stats_ObserveItemsLoad(void); diff --git a/src/tr1/game/stats/types.h b/src/libtrx/include/libtrx/game/stats/types.h similarity index 79% rename from src/tr1/game/stats/types.h rename to src/libtrx/include/libtrx/game/stats/types.h index 6c6d1a71f..64a82be99 100644 --- a/src/tr1/game/stats/types.h +++ b/src/libtrx/include/libtrx/game/stats/types.h @@ -5,15 +5,18 @@ typedef struct STATS_COMMON { uint32_t timer; uint32_t kill_count; + uint32_t ammo_used; + uint32_t ammo_hits; + uint32_t distance_travelled; + uint16_t max_secret_count; + double medipacks_used; +#if TR_VERSION == 1 uint16_t secret_count; uint16_t pickup_count; uint32_t max_kill_count; - uint16_t max_secret_count; uint16_t max_pickup_count; - uint32_t ammo_hits; - uint32_t ammo_used; - double medipacks_used; - uint32_t distance_travelled; + int32_t death_count; +#endif } STATS_COMMON; typedef struct { @@ -23,4 +26,8 @@ typedef struct { typedef struct { struct STATS_COMMON; +#if TR_VERSION == 2 + int32_t found_secrets; + int32_t total_secrets; +#endif } FINAL_STATS; diff --git a/src/libtrx/include/libtrx/game/text.h b/src/libtrx/include/libtrx/game/text.h index d4355aa72..7385805eb 100644 --- a/src/libtrx/include/libtrx/game/text.h +++ b/src/libtrx/include/libtrx/game/text.h @@ -1,5 +1,8 @@ #pragma once +#include "./output/draw.h" + +#include #include // TODO: rename this @@ -31,12 +34,6 @@ typedef struct { } combine_with; } GLYPH_INFO; -typedef enum { - TS_HEADING = 0, - TS_BACKGROUND = 1, - TS_REQUESTED = 2, -} TEXT_STYLE; - typedef struct { union { uint32_t all; @@ -94,8 +91,10 @@ typedef struct { TEXT_STYLE style; } outline; + size_t content_cap; char *content; + size_t glyphs_cap; const GLYPH_INFO **glyphs; } TEXTSTRING; diff --git a/src/libtrx/include/libtrx/game/ui.h b/src/libtrx/include/libtrx/game/ui.h new file mode 100644 index 000000000..811c320db --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui.h @@ -0,0 +1,27 @@ +#pragma once + +#include "./ui/common.h" +#include "./ui/dialogs/controls.h" +#include "./ui/dialogs/controls_backend.h" +#include "./ui/dialogs/examine_item.h" +#include "./ui/dialogs/new_game.h" +#include "./ui/dialogs/pause.h" +#include "./ui/dialogs/photo_mode.h" +#include "./ui/dialogs/play_any_level.h" +#include "./ui/dialogs/save_slot.h" +#include "./ui/dialogs/select_level.h" +#include "./ui/dialogs/stats.h" +#include "./ui/elements/anchor.h" +#include "./ui/elements/fade.h" +#include "./ui/elements/flash.h" +#include "./ui/elements/frame.h" +#include "./ui/elements/label.h" +#include "./ui/elements/modal.h" +#include "./ui/elements/pad.h" +#include "./ui/elements/requester.h" +#include "./ui/elements/resize.h" +#include "./ui/elements/spacer.h" +#include "./ui/elements/stack.h" +#include "./ui/events.h" +#include "./ui/hud/console.h" +#include "./ui/hud/console_logs.h" diff --git a/src/libtrx/include/libtrx/game/ui/common.h b/src/libtrx/include/libtrx/game/ui/common.h index e56325eb9..6400264f1 100644 --- a/src/libtrx/include/libtrx/game/ui/common.h +++ b/src/libtrx/include/libtrx/game/ui/common.h @@ -1,6 +1,7 @@ #pragma once -#include "./events.h" +#include +#include typedef enum { UI_KEY_UP, @@ -14,6 +15,56 @@ typedef enum { UI_KEY_ESCAPE, } UI_INPUT; +// Forward declaration of the node and its vtable. +struct UI_NODE; +typedef struct { + void (*measure)(struct UI_NODE *node); + void (*layout)(struct UI_NODE *node, float x, float y, float w, float h); + void (*draw)(const struct UI_NODE *node); +} UI_WIDGET_OPS; + +// Node structure that forms the UI tree +typedef struct UI_NODE { + // Common operations on a widget + const UI_WIDGET_OPS *ops; + + // Final layout rectangle + float x; + float y; + float w; + float h; + + // Needed size from measure pass + float measure_w; + float measure_h; + + // Link to parent and siblings to form a tree + struct UI_NODE *parent; + struct UI_NODE *first_child; + struct UI_NODE *last_child; + struct UI_NODE *next_sibling; + + // Widget-specific data + void *data; +} UI_NODE; + +// Dimensions in virtual pixels of the screen area +// (640x480 for any 4:3 resolution on 1.00 text scaling) +extern int32_t UI_GetCanvasWidth(void); +extern int32_t UI_GetCanvasHeight(void); +extern float UI_ScaleX(float x); +extern float UI_ScaleY(float y); + +// Public API for scene management +void UI_BeginScene(void); +void UI_EndScene(void); + +// Helpers to add children, etc. +UI_NODE *UI_AllocNode(const UI_WIDGET_OPS *ops, size_t additional_size); +void UI_AddChild(UI_NODE *child); +void UI_PushCurrent(UI_NODE *child); +void UI_PopCurrent(void); + void UI_Init(void); void UI_Shutdown(void); void UI_ToggleState(bool *config_setting); @@ -21,8 +72,3 @@ void UI_ToggleState(bool *config_setting); void UI_HandleKeyDown(uint32_t key); void UI_HandleKeyUp(uint32_t key); void UI_HandleTextEdit(const char *text); -void UI_HandleLayoutChange(void); - -extern int32_t UI_GetCanvasWidth(void); -extern int32_t UI_GetCanvasHeight(void); -extern UI_INPUT UI_TranslateInput(uint32_t system_keycode); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/base_passport.h b/src/libtrx/include/libtrx/game/ui/dialogs/base_passport.h new file mode 100644 index 000000000..ccf1dcb05 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/base_passport.h @@ -0,0 +1,16 @@ +// Base passport dialog functions. +// Does not implement a function on its own, and is used mostly for placement +// and sizing of the larger dialogs such as load/save game. + +#pragma once + +#include "../common.h" +#include "../elements/requester.h" + +// state functions +void UI_BasePassportDialog_Init(UI_REQUESTER_STATE *req, size_t max_rows); +void UI_BasePassportDialog_Control(UI_REQUESTER_STATE *req); + +// draw functions +void UI_BeginBasePassportDialog(void); +void UI_EndBasePassportDialog(void); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/controls.h b/src/libtrx/include/libtrx/game/ui/dialogs/controls.h new file mode 100644 index 000000000..f119f47eb --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/controls.h @@ -0,0 +1,27 @@ +#pragma once + +// A controls editor dialog. + +#include "../../../event_manager.h" +#include "../../input.h" +#include "../common.h" +#include "./controls_backend.h" +#include "./controls_editor.h" + +typedef struct { + int32_t phase; + INPUT_BACKEND backend; + int32_t active_layout; + + EVENT_MANAGER *events; + UI_CONTROLS_BACKEND_STATE backend_state; + UI_CONTROLS_EDITOR_STATE editor_state; +} UI_CONTROLS_STATE; + +// state functions +void UI_Controls_Init(UI_CONTROLS_STATE *s); +void UI_Controls_Free(UI_CONTROLS_STATE *s); +bool UI_Controls_Control(UI_CONTROLS_STATE *s); + +// draw functions +void UI_Controls(UI_CONTROLS_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/controls_backend.h b/src/libtrx/include/libtrx/game/ui/dialogs/controls_backend.h new file mode 100644 index 000000000..454c29813 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/controls_backend.h @@ -0,0 +1,18 @@ +#pragma once + +// A control backend (keyboard/controller) choice dialog. + +#include "../common.h" +#include "../elements/requester.h" + +typedef struct { + UI_REQUESTER_STATE req; +} UI_CONTROLS_BACKEND_STATE; + +// state functions +void UI_ControlsBackend_Init(UI_CONTROLS_BACKEND_STATE *s); +void UI_ControlsBackend_Free(UI_CONTROLS_BACKEND_STATE *s); +int32_t UI_ControlsBackend_Control(UI_CONTROLS_BACKEND_STATE *s); + +// draw functions +void UI_ControlsBackend(UI_CONTROLS_BACKEND_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/controls_editor.h b/src/libtrx/include/libtrx/game/ui/dialogs/controls_editor.h new file mode 100644 index 000000000..6ce2f8826 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/controls_editor.h @@ -0,0 +1,36 @@ +#pragma once + +// A controls remapper dialog. + +#include "../../../event_manager.h" +#include "../../input.h" +#include "../common.h" +#include "../elements/flash.h" +#include "../elements/requester.h" + +typedef struct { + int32_t phase; + INPUT_BACKEND backend; + int32_t active_layout; + INPUT_ROLE active_role; + int32_t active_col; + int32_t active_row; + UI_FLASH_STATE flash; + EVENT_MANAGER *events; +} UI_CONTROLS_EDITOR_STATE; + +typedef enum { + UI_CONTROLS_CHOICE_EXIT, + UI_CONTROLS_CHOICE_GO_BACK, + UI_CONTROLS_CHOICE_NOOP, +} UI_CONTROLS_CHOICE; + +// state functions +void UI_ControlsEditor_Init(UI_CONTROLS_EDITOR_STATE *s, EVENT_MANAGER *events); +void UI_ControlsEditor_Free(UI_CONTROLS_EDITOR_STATE *s); +void UI_ControlsEditor_Reinit( + UI_CONTROLS_EDITOR_STATE *s, INPUT_BACKEND backend, int32_t layout); +UI_CONTROLS_CHOICE UI_ControlsEditor_Control(UI_CONTROLS_EDITOR_STATE *s); + +// draw functions +void UI_ControlsEditor(UI_CONTROLS_EDITOR_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/examine_item.h b/src/libtrx/include/libtrx/game/ui/dialogs/examine_item.h new file mode 100644 index 000000000..f543ab718 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/examine_item.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../../../vector.h" +#include "../common.h" + +// A widget to cycle through several pages of a text content. + +typedef struct { + char *title; + size_t max_lines; + int32_t current_page; + VECTOR *page_content; + bool is_empty; +} UI_EXAMINE_ITEM_STATE; + +void UI_ExamineItem_Init( + UI_EXAMINE_ITEM_STATE *state, const char *title, const char *text, + size_t max_lines); +void UI_ExamineItem_Control(UI_EXAMINE_ITEM_STATE *state); +void UI_ExamineItem_Free(UI_EXAMINE_ITEM_STATE *state); + +void UI_ExamineItem(UI_EXAMINE_ITEM_STATE *state); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/new_game.h b/src/libtrx/include/libtrx/game/ui/dialogs/new_game.h new file mode 100644 index 000000000..67d408b17 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/new_game.h @@ -0,0 +1,18 @@ +#pragma once + +// A new game mode selector dialog. + +#include "../common.h" +#include "../elements/requester.h" + +typedef struct { + UI_REQUESTER_STATE req; +} UI_NEW_GAME_STATE; + +// state functions +void UI_NewGame_Init(UI_NEW_GAME_STATE *s); +int32_t UI_NewGame_Control(UI_NEW_GAME_STATE *s); +void UI_NewGame_Free(UI_NEW_GAME_STATE *s); + +// draw functions +void UI_NewGame(UI_NEW_GAME_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/pause.h b/src/libtrx/include/libtrx/game/ui/dialogs/pause.h new file mode 100644 index 000000000..1bb7be76e --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/pause.h @@ -0,0 +1,26 @@ +#pragma once + +// A pause exit confirmation dialog. + +#include "../common.h" +#include "../elements/requester.h" + +typedef struct { + int32_t phase; + UI_REQUESTER_STATE req; +} UI_PAUSE_STATE; + +typedef enum { + UI_PAUSE_NOOP, + UI_PAUSE_RESUME_PAUSE, + UI_PAUSE_EXIT_TO_GAME, + UI_PAUSE_EXIT_TO_TITLE, +} UI_PAUSE_EXIT_CHOICE; + +// state functions +void UI_Pause_Init(UI_PAUSE_STATE *s); +UI_PAUSE_EXIT_CHOICE UI_Pause_Control(UI_PAUSE_STATE *s); +void UI_Pause_Free(UI_PAUSE_STATE *s); + +// draw functions +void UI_Pause(UI_PAUSE_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/photo_mode.h b/src/libtrx/include/libtrx/game/ui/dialogs/photo_mode.h new file mode 100644 index 000000000..eafbf1df0 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/photo_mode.h @@ -0,0 +1,7 @@ +#pragma once + +#include "../common.h" + +// A photo mode tutorial dialog. + +void UI_PhotoMode(void); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/play_any_level.h b/src/libtrx/include/libtrx/game/ui/dialogs/play_any_level.h new file mode 100644 index 000000000..4783bf820 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/play_any_level.h @@ -0,0 +1,20 @@ +// UI dialog for selecting a level within a save slot + +#pragma once + +#include "../common.h" + +#include + +#define UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE UI_REQUESTER_NO_CHOICE +#define UI_PLAY_ANY_LEVEL_CHOICE_CANCEL UI_REQUESTER_CANCEL + +typedef struct UI_PLAY_ANY_LEVEL_DIALOG_STATE UI_PLAY_ANY_LEVEL_DIALOG_STATE; + +struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *UI_PlayAnyLevelDialog_Init(void); + +void UI_PlayAnyLevelDialog_Free(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s); + +int32_t UI_PlayAnyLevelDialog_Control(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s); + +void UI_PlayAnyLevelDialog(struct UI_PLAY_ANY_LEVEL_DIALOG_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/save_slot.h b/src/libtrx/include/libtrx/game/ui/dialogs/save_slot.h new file mode 100644 index 000000000..7b13241d9 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/save_slot.h @@ -0,0 +1,34 @@ +// UI dialog for selecting a save slot (load or save game) + +#pragma once + +#include "../common.h" + +typedef enum { + UI_SAVE_SLOT_DIALOG_LOAD_GAME, + UI_SAVE_SLOT_DIALOG_SAVE_GAME, +} UI_SAVE_SLOT_DIALOG_TYPE; + +typedef enum { + UI_SAVE_SLOT_DIALOG_NO_CHOICE, + UI_SAVE_SLOT_DIALOG_CANCEL, + UI_SAVE_SLOT_DIALOG_CONFIRM, + UI_SAVE_SLOT_DIALOG_DETAILS, +} UI_SAVE_SLOT_DIALOG_ACTION; + +typedef struct { + UI_SAVE_SLOT_DIALOG_ACTION action; + int32_t slot_num; +} UI_SAVE_SLOT_DIALOG_CHOICE; + +typedef struct UI_SAVE_SLOT_DIALOG_STATE UI_SAVE_SLOT_DIALOG_STATE; + +// state functions +struct UI_SAVE_SLOT_DIALOG_STATE *UI_SaveSlotDialog_Init( + UI_SAVE_SLOT_DIALOG_TYPE type, int32_t save_slot); +void UI_SaveSlotDialog_Free(struct UI_SAVE_SLOT_DIALOG_STATE *s); +UI_SAVE_SLOT_DIALOG_CHOICE UI_SaveSlotDialog_Control( + struct UI_SAVE_SLOT_DIALOG_STATE *s); + +// draw functions +void UI_SaveSlotDialog(const struct UI_SAVE_SLOT_DIALOG_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/dialogs/select_level.h b/src/libtrx/include/libtrx/game/ui/dialogs/select_level.h new file mode 100644 index 000000000..21cd97066 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/dialogs/select_level.h @@ -0,0 +1,22 @@ +// UI dialog for selecting a level within a save slot + +#pragma once + +#include "../common.h" + +#include + +#define UI_SELECT_LEVEL_CHOICE_NOOP -1 +#define UI_SELECT_LEVEL_CHOICE_CANCEL -2 +#define UI_SELECT_LEVEL_CHOICE_PLAY_STORY_SO_FAR -3 + +typedef struct UI_SELECT_LEVEL_DIALOG_STATE UI_SELECT_LEVEL_DIALOG_STATE; + +struct UI_SELECT_LEVEL_DIALOG_STATE *UI_SelectLevelDialog_Init( + int32_t save_slot); + +void UI_SelectLevelDialog_Free(struct UI_SELECT_LEVEL_DIALOG_STATE *s); + +int32_t UI_SelectLevelDialog_Control(struct UI_SELECT_LEVEL_DIALOG_STATE *s); + +void UI_SelectLevelDialog(struct UI_SELECT_LEVEL_DIALOG_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/stats_dialog.h b/src/libtrx/include/libtrx/game/ui/dialogs/stats.h similarity index 50% rename from src/libtrx/include/libtrx/game/ui/widgets/stats_dialog.h rename to src/libtrx/include/libtrx/game/ui/dialogs/stats.h index 62c1d58b0..408310c45 100644 --- a/src/libtrx/include/libtrx/game/ui/widgets/stats_dialog.h +++ b/src/libtrx/include/libtrx/game/ui/dialogs/stats.h @@ -1,6 +1,7 @@ #pragma once -#include "./base.h" +#include "../common.h" +#include "../elements/requester.h" typedef enum { UI_STATS_DIALOG_MODE_LEVEL, @@ -21,4 +22,13 @@ typedef struct { int32_t level_num; } UI_STATS_DIALOG_ARGS; -UI_WIDGET *UI_StatsDialog_Create(UI_STATS_DIALOG_ARGS args); +typedef struct { + UI_STATS_DIALOG_ARGS args; + UI_REQUESTER_STATE assault_req; +} UI_STATS_DIALOG_STATE; + +void UI_StatsDialog_Init(UI_STATS_DIALOG_STATE *s, UI_STATS_DIALOG_ARGS args); +void UI_StatsDialog_Free(UI_STATS_DIALOG_STATE *s); +int32_t UI_StatsDialog_Control(UI_STATS_DIALOG_STATE *s); + +extern void UI_StatsDialog(UI_STATS_DIALOG_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/elements/anchor.h b/src/libtrx/include/libtrx/game/ui/elements/anchor.h new file mode 100644 index 000000000..e843d5390 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/anchor.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../common.h" + +// Used to align a top-level widget to the screen center or to the screen edges. +// Uses ratio inputs. + +void UI_BeginAnchor(const float x, const float y); +void UI_EndAnchor(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/fade.h b/src/libtrx/include/libtrx/game/ui/elements/fade.h new file mode 100644 index 000000000..cf1de65fe --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/fade.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../../fader.h" +#include "../common.h" + +#include + +// Draw a fade on top or behind children. + +void UI_BeginFade(FADER *fader, bool is_on_top); +void UI_EndFade(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/fixed.h b/src/libtrx/include/libtrx/game/ui/elements/fixed.h new file mode 100644 index 000000000..f6ab5d9b0 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/fixed.h @@ -0,0 +1,10 @@ +#pragma once + +#include "../common.h" + +// A transformer to detach the children from the parent layout. +// x, y specify the anchor point - 0,0 mean top left corner, 1,1 mean bottom +// right corner. + +void UI_BeginFixed(float x, float y); +void UI_EndFixed(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/flash.h b/src/libtrx/include/libtrx/game/ui/elements/flash.h new file mode 100644 index 000000000..1254fdf7f --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/flash.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../common.h" + +#include + +// Make the child widget invisible in the specified interval. + +typedef struct { + int32_t rate; + int32_t count; +} UI_FLASH_STATE; + +// state functions +void UI_Flash_Init(UI_FLASH_STATE *s, int32_t rate); +void UI_Flash_Free(UI_FLASH_STATE *s); +void UI_Flash_Control(UI_FLASH_STATE *s); + +// draw functions +void UI_BeginFlash(UI_FLASH_STATE *s); +void UI_EndFlash(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/frame.h b/src/libtrx/include/libtrx/game/ui/elements/frame.h new file mode 100644 index 000000000..1be4f0fdd --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/frame.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../common.h" + +// A frame around the child widget. + +typedef enum { + UI_FRAME_DIALOG_BACKGROUND, + UI_FRAME_DIALOG_HEADING, + UI_FRAME_SELECTED_OPTION, + UI_FRAME_OUTLINE_ONLY, +} UI_FRAME_STYLE; + +void UI_BeginFrame(UI_FRAME_STYLE style); +void UI_EndFrame(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/hide.h b/src/libtrx/include/libtrx/game/ui/elements/hide.h new file mode 100644 index 000000000..f7c3ad1c9 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/hide.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../common.h" + +// Make the child widget invisible if the given flag is true. +// The children still take up their size. + +void UI_BeginHide(bool hide_children); +void UI_EndHide(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/label.h b/src/libtrx/include/libtrx/game/ui/elements/label.h new file mode 100644 index 000000000..32ce8122e --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/label.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../common.h" + +#include + +// Basic text widget. + +typedef struct { + float scale; + int32_t z; +} UI_LABEL_SETTINGS; + +void UI_Label(const char *text); +void UI_LabelEx(const char *text, UI_LABEL_SETTINGS settings); + +void UI_Label_Measure(const char *text, float *out_w, float *out_h); +void UI_Label_MeasureEx( + const char *text, float *out_w, float *out_h, UI_LABEL_SETTINGS settings); diff --git a/src/libtrx/include/libtrx/game/ui/elements/modal.h b/src/libtrx/include/libtrx/game/ui/elements/modal.h new file mode 100644 index 000000000..8d1f81a7b --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/modal.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../common.h" + +// A widget that resizes the children to the canvas size +// and places it at a specific proportional spot. + +void UI_BeginModal(float x, float y); +void UI_EndModal(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/offset.h b/src/libtrx/include/libtrx/game/ui/elements/offset.h new file mode 100644 index 000000000..bd8710545 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/offset.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../common.h" + +// A transformer to move the child element in a certain direction. +// Does not affect the occupied space. + +void UI_BeginOffset(float x, float y); +void UI_EndOffset(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/pad.h b/src/libtrx/include/libtrx/game/ui/elements/pad.h new file mode 100644 index 000000000..7deff9aee --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/pad.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../common.h" + +// An invisible border in pixel units around the child widget. + +void UI_BeginPad(float x, float y); +void UI_BeginPadEx(float l, float r, float t, float d); +void UI_EndPad(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/prompt.h b/src/libtrx/include/libtrx/game/ui/elements/prompt.h new file mode 100644 index 000000000..61670c00e --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/prompt.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../common.h" +#include "./flash.h" + +#include + +// A text edit widget that collects text input from the player. +// Needs to be in focus to work, otherwise is inactive. + +typedef struct { + bool is_focused; + int32_t caret_pos; + int32_t current_text_capacity; + char *current_text; + + int32_t listener1; + int32_t listener2; + + UI_FLASH_STATE flash; +} UI_PROMPT_STATE; + +// state functions +void UI_Prompt_Init(UI_PROMPT_STATE *s); +void UI_Prompt_Free(UI_PROMPT_STATE *s); +void UI_Prompt_Control(UI_PROMPT_STATE *s); +void UI_Prompt_Clear(UI_PROMPT_STATE *s); +void UI_Prompt_SetFocus(UI_PROMPT_STATE *s, bool is_focused); +void UI_Prompt_ChangeText(UI_PROMPT_STATE *s, const char *new_text); + +// draw functions +void UI_Prompt(UI_PROMPT_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/elements/requester.h b/src/libtrx/include/libtrx/game/ui/elements/requester.h new file mode 100644 index 000000000..0763dccd5 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/requester.h @@ -0,0 +1,43 @@ +#pragma once + +// A window to select a single option from a list of predefined choices. + +#include "../common.h" + +#include + +#define UI_REQUESTER_CANCEL -2 +#define UI_REQUESTER_NO_CHOICE -1 + +typedef struct { + bool is_selectable; + int32_t vis_rows; + int32_t max_rows; + int32_t vis_row; + int32_t sel_row; + float row_pad; + float row_spacing; + bool show_arrows; + bool reserve_space; +} UI_REQUESTER_STATE; + +// state functions +void UI_Requester_Init( + UI_REQUESTER_STATE *s, int32_t vis_rows, int32_t max_rows, + bool is_selectable); +void UI_Requester_Free(UI_REQUESTER_STATE *s); +int32_t UI_Requester_Control(UI_REQUESTER_STATE *s); +void UI_Requester_SetMaxRows(UI_REQUESTER_STATE *s, size_t max_rows); +void UI_Requester_SetVisibleRows(UI_REQUESTER_STATE *s, size_t visible_rows); +int32_t UI_Requester_GetFirstRow(const UI_REQUESTER_STATE *s); +int32_t UI_Requester_GetLastRow(const UI_REQUESTER_STATE *s); +int32_t UI_Requester_GetCurrentRow(const UI_REQUESTER_STATE *s); +bool UI_Requester_IsRowVisible(const UI_REQUESTER_STATE *s, int32_t i); +bool UI_Requester_IsRowSelected(const UI_REQUESTER_STATE *s, int32_t i); + +// draw functions +void UI_BeginRequester(const UI_REQUESTER_STATE *s, const char *title); +void UI_EndRequester(const UI_REQUESTER_STATE *s); + +void UI_BeginRequesterRow(const UI_REQUESTER_STATE *s, int32_t i); +void UI_EndRequesterRow(const UI_REQUESTER_STATE *s, int32_t i); diff --git a/src/libtrx/include/libtrx/game/ui/elements/resize.h b/src/libtrx/include/libtrx/game/ui/elements/resize.h new file mode 100644 index 000000000..a15efe57c --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/resize.h @@ -0,0 +1,10 @@ +#pragma once + +#include "../common.h" + +// Resize child widget to a specified size in pixels. +// A negative size means to use the child's size. +// A zero value means to hide the child, but participate in the layout pass. + +void UI_BeginResize(float x, float y); +void UI_EndResize(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/spacer.h b/src/libtrx/include/libtrx/game/ui/elements/spacer.h new file mode 100644 index 000000000..bd76a436e --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/spacer.h @@ -0,0 +1,7 @@ +#pragma once + +#include "../common.h" + +// An invisible widget that occupies certain space in pixels. + +void UI_Spacer(float w, float h); diff --git a/src/libtrx/include/libtrx/game/ui/elements/stack.h b/src/libtrx/include/libtrx/game/ui/elements/stack.h new file mode 100644 index 000000000..a7fb9336f --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/stack.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../common.h" + +#include + +// Stack several widgets vertically or horizontally. + +typedef enum { + UI_STACK_VERTICAL, + UI_STACK_HORIZONTAL, +} UI_STACK_ORIENTATION; + +typedef enum { + UI_STACK_H_ALIGN_LEFT, + UI_STACK_H_ALIGN_CENTER, + UI_STACK_H_ALIGN_RIGHT, + UI_STACK_H_ALIGN_SPAN, + UI_STACK_H_ALIGN_DISTRIBUTE, +} UI_STACK_H_ALIGN; + +typedef enum { + UI_STACK_V_ALIGN_TOP, + UI_STACK_V_ALIGN_CENTER, + UI_STACK_V_ALIGN_BOTTOM, + UI_STACK_V_ALIGN_SPAN, + UI_STACK_V_ALIGN_DISTRIBUTE, +} UI_STACK_V_ALIGN; + +typedef struct { + UI_STACK_ORIENTATION orientation; + struct { + UI_STACK_H_ALIGN h; + UI_STACK_V_ALIGN v; + } align; + struct { + float h; + float v; + } spacing; +} UI_STACK_SETTINGS; + +void UI_BeginStack(UI_STACK_ORIENTATION orientation); +void UI_BeginStackEx(UI_STACK_SETTINGS settings); +void UI_EndStack(void); diff --git a/src/libtrx/include/libtrx/game/ui/elements/window.h b/src/libtrx/include/libtrx/game/ui/elements/window.h new file mode 100644 index 000000000..561f7b996 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/elements/window.h @@ -0,0 +1,7 @@ +#pragma once + +void UI_BeginWindow(void); +void UI_EndWindow(void); +void UI_BeginWindowBody(void); +void UI_EndWindowBody(void); +void UI_WindowTitle(const char *title); diff --git a/src/libtrx/include/libtrx/game/ui/events.h b/src/libtrx/include/libtrx/game/ui/events.h index 28c699e35..31c235332 100644 --- a/src/libtrx/include/libtrx/game/ui/events.h +++ b/src/libtrx/include/libtrx/game/ui/events.h @@ -1,17 +1,16 @@ #pragma once #include "../../event_manager.h" -#include "./widgets/base.h" typedef void (*EVENT_LISTENER)(const EVENT *, void *user_data); -void UI_Events_Init(void); -void UI_Events_Shutdown(void); +void UI_InitEvents(void); +void UI_ShutdownEvents(void); -int32_t UI_Events_Subscribe( - const char *event_name, const UI_WIDGET *sender, EVENT_LISTENER listener, +int32_t UI_Subscribe( + const char *event_name, const void *sender, EVENT_LISTENER listener, void *user_data); -void UI_Events_Unsubscribe(int32_t listener_id); +void UI_Unsubscribe(int32_t listener_id); -void UI_Events_Fire(const EVENT *event); +void UI_FireEvent(EVENT event); diff --git a/src/libtrx/include/libtrx/game/ui/hud/console.h b/src/libtrx/include/libtrx/game/ui/hud/console.h new file mode 100644 index 000000000..97241df44 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/hud/console.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../elements/prompt.h" +#include "./console_logs.h" + +// Dev console display widget. + +typedef struct { + UI_CONSOLE_LOGS logs; + UI_PROMPT_STATE prompt; + + int32_t listeners[5]; + int32_t history_idx; +} UI_CONSOLE_STATE; + +// state functions +void UI_Console_Init(UI_CONSOLE_STATE *s); +void UI_Console_Free(UI_CONSOLE_STATE *s); +void UI_Console_Control(UI_CONSOLE_STATE *s); + +// draw functions +void UI_Console(UI_CONSOLE_STATE *s); diff --git a/src/libtrx/include/libtrx/game/ui/hud/console_logs.h b/src/libtrx/include/libtrx/game/ui/hud/console_logs.h new file mode 100644 index 000000000..0f97938e6 --- /dev/null +++ b/src/libtrx/include/libtrx/game/ui/hud/console_logs.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +// Scrollback for the dev console. + +typedef struct { + char *text; + double expire_at; +} UI_CONSOLE_LOG_LINE; + +typedef struct { + size_t max_lines; + size_t vis_lines; + UI_CONSOLE_LOG_LINE *logs; + int32_t listener_id; +} UI_CONSOLE_LOGS; + +// state functions +void UI_ConsoleLogs_Init(UI_CONSOLE_LOGS *s); +void UI_ConsoleLogs_Free(UI_CONSOLE_LOGS *s); +void UI_ConsoleLogs(UI_CONSOLE_LOGS *s); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/base.h b/src/libtrx/include/libtrx/game/ui/widgets/base.h deleted file mode 100644 index 2c8fae478..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/base.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -struct UI_WIDGET; - -typedef void (*UI_WIDGET_CONTROL)(struct UI_WIDGET *self); -typedef void (*UI_WIDGET_DRAW)(struct UI_WIDGET *self); -typedef int32_t (*UI_WIDGET_GET_WIDTH)(const struct UI_WIDGET *self); -typedef int32_t (*UI_WIDGET_GET_HEIGHT)(const struct UI_WIDGET *self); -typedef void (*UI_WIDGET_SET_POSITION)( - struct UI_WIDGET *self, int32_t x, int32_t y); -typedef void (*UI_WIDGET_FREE)(struct UI_WIDGET *self); - -typedef struct UI_WIDGET { - UI_WIDGET_CONTROL control; - UI_WIDGET_DRAW draw; - UI_WIDGET_GET_WIDTH get_width; - UI_WIDGET_GET_HEIGHT get_height; - UI_WIDGET_SET_POSITION set_position; - UI_WIDGET_FREE free; - bool is_hidden; -} UI_WIDGET; - -typedef UI_WIDGET UI_WIDGET_VTABLE; diff --git a/src/libtrx/include/libtrx/game/ui/widgets/console.h b/src/libtrx/include/libtrx/game/ui/widgets/console.h deleted file mode 100644 index 26ddacdfb..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/console.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "./base.h" - -UI_WIDGET *UI_Console_Create(void); - -void UI_Console_HandleOpen(UI_WIDGET *widget); -void UI_Console_HandleClose(UI_WIDGET *widget); -void UI_Console_HandleLog(UI_WIDGET *widget, const char *text); -void UI_Console_ScrollLogs(UI_WIDGET *widget); -int32_t UI_Console_GetVisibleLogCount(UI_WIDGET *widget); -int32_t UI_Console_GetMaxLogCount(UI_WIDGET *widget); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/frame.h b/src/libtrx/include/libtrx/game/ui/widgets/frame.h deleted file mode 100644 index 81e8cb4e2..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/frame.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include "./base.h" - -UI_WIDGET *UI_Frame_Create(UI_WIDGET *root, int32_t margin, int32_t padding); -void UI_Frame_SetFrameVisible(UI_WIDGET *widget, bool is_frame_visible); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/label.h b/src/libtrx/include/libtrx/game/ui/widgets/label.h deleted file mode 100644 index fdf2c7bdf..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/label.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "./base.h" - -#define UI_LABEL_AUTO_SIZE (-1) - -UI_WIDGET *UI_Label_Create(const char *text, int32_t width, int32_t height); - -void UI_Label_ChangeText(UI_WIDGET *widget, const char *text); -const char *UI_Label_GetText(UI_WIDGET *widget); -void UI_Label_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); -void UI_Label_SetVisible(UI_WIDGET *widget, bool visible); - -void UI_Label_AddFrame(UI_WIDGET *widget); -void UI_Label_RemoveFrame(UI_WIDGET *widget); -void UI_Label_Flash(UI_WIDGET *widget, bool enable, int32_t rate); -void UI_Label_SetScale(UI_WIDGET *widget, float scale); -void UI_Label_SetZIndex(UI_WIDGET *widget, int32_t z_index); -int32_t UI_Label_MeasureTextWidth(UI_WIDGET *widget); -int32_t UI_Label_MeasureTextHeight(UI_WIDGET *widget); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/photo_mode.h b/src/libtrx/include/libtrx/game/ui/widgets/photo_mode.h deleted file mode 100644 index bb8babe65..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/photo_mode.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "./base.h" - -UI_WIDGET *UI_PhotoMode_Create(void); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/prompt.h b/src/libtrx/include/libtrx/game/ui/widgets/prompt.h deleted file mode 100644 index 91e51d97d..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/prompt.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "./base.h" - -UI_WIDGET *UI_Prompt_Create(int32_t width, int32_t height); -void UI_Prompt_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); - -void UI_Prompt_SetFocus(UI_WIDGET *widget, bool is_focused); -void UI_Prompt_Clear(UI_WIDGET *widget); -void UI_Prompt_ChangeText(UI_WIDGET *widget, const char *new_text); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/requester.h b/src/libtrx/include/libtrx/game/ui/widgets/requester.h deleted file mode 100644 index c30848b83..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/requester.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "./base.h" - -typedef struct { - bool is_selectable; - int32_t visible_rows; - int32_t width; - int32_t row_height; -} UI_REQUESTER_SETTINGS; - -UI_WIDGET *UI_Requester_Create(UI_REQUESTER_SETTINGS settings); -int32_t UI_Requester_GetSelectedRow(UI_WIDGET *requester); -void UI_Requester_ClearRows(UI_WIDGET *requester); -void UI_Requester_SetTitle(UI_WIDGET *requester, const char *title); -void UI_Requester_AddRowLR( - UI_WIDGET *requester, const char *text_l, const char *text_r, - void *user_data); -void UI_Requester_AddRowC( - UI_WIDGET *requester, const char *text, void *user_data); -void *UI_Requester_GetRowUserData(UI_WIDGET *widget, int32_t idx); -int32_t UI_Requester_GetRowCount(UI_WIDGET *widget); -void UI_Requester_ChangeRowLR( - UI_WIDGET *widget, int32_t idx, const char *text_l, const char *text_r, - void *user_data); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/spacer.h b/src/libtrx/include/libtrx/game/ui/widgets/spacer.h deleted file mode 100644 index 9c20e210d..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/spacer.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include "./base.h" - -UI_WIDGET *UI_Spacer_Create(int32_t width, int32_t height); -void UI_Spacer_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/stack.h b/src/libtrx/include/libtrx/game/ui/widgets/stack.h deleted file mode 100644 index 1f1d675c7..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/stack.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "./base.h" - -#define UI_STACK_AUTO_SIZE (-1) - -typedef enum { - UI_STACK_LAYOUT_HORIZONTAL, - UI_STACK_LAYOUT_VERTICAL, -} UI_STACK_LAYOUT; - -typedef enum { - UI_STACK_H_ALIGN_LEFT, - UI_STACK_H_ALIGN_CENTER, - UI_STACK_H_ALIGN_RIGHT, - UI_STACK_H_ALIGN_DISTRIBUTE, -} UI_STACK_H_ALIGN; - -typedef enum { - UI_STACK_V_ALIGN_TOP, - UI_STACK_V_ALIGN_CENTER, - UI_STACK_V_ALIGN_BOTTOM, - UI_STACK_V_ALIGN_DISTRIBUTE, -} UI_STACK_V_ALIGN; - -UI_WIDGET *UI_Stack_Create( - UI_STACK_LAYOUT layout, int32_t width, int32_t height); -void UI_Stack_SetHAlign(UI_WIDGET *self, UI_STACK_H_ALIGN align); -void UI_Stack_SetVAlign(UI_WIDGET *self, UI_STACK_V_ALIGN align); -void UI_Stack_ClearChildren(UI_WIDGET *self); -void UI_Stack_AddChild(UI_WIDGET *self, UI_WIDGET *child); -void UI_Stack_SetSize(UI_WIDGET *widget, int32_t width, int32_t height); -void UI_Stack_DoLayout(UI_WIDGET *self); diff --git a/src/libtrx/include/libtrx/game/ui/widgets/window.h b/src/libtrx/include/libtrx/game/ui/widgets/window.h deleted file mode 100644 index 33f896433..000000000 --- a/src/libtrx/include/libtrx/game/ui/widgets/window.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "./base.h" - -UI_WIDGET *UI_Window_Create( - UI_WIDGET *root, int32_t border_top, int32_t border_right, - int32_t border_bottom, int32_t border_left); - -void UI_Window_SetTitle(UI_WIDGET *widget, const char *text); -void UI_Window_SetRootWidget(UI_WIDGET *widget, UI_WIDGET *root); diff --git a/src/libtrx/include/libtrx/game/viewport.h b/src/libtrx/include/libtrx/game/viewport.h index cd234ea38..4f9694117 100644 --- a/src/libtrx/include/libtrx/game/viewport.h +++ b/src/libtrx/include/libtrx/game/viewport.h @@ -11,5 +11,7 @@ extern int16_t Viewport_GetFOV(bool resolve_user_fov); extern void Viewport_AlterFOV(int16_t view_angle); #endif +extern int32_t Viewport_GetWidth(void); +extern int32_t Viewport_GetHeight(void); extern int32_t Viewport_GetMaxX(void); extern int32_t Viewport_GetMaxY(void); diff --git a/src/libtrx/include/libtrx/gfx/context.h b/src/libtrx/include/libtrx/gfx/context.h index 398a9ff66..ad94e83aa 100644 --- a/src/libtrx/include/libtrx/gfx/context.h +++ b/src/libtrx/include/libtrx/gfx/context.h @@ -37,6 +37,5 @@ void GFX_Context_SwitchToDisplayViewport(void); void GFX_Context_ScheduleScreenshot(const char *path); const char *GFX_Context_GetScheduledScreenshotPath(void); void GFX_Context_ClearScheduledScreenshotPath(void); -void GFX_Context_GetScale(float *out_x, float *out_y); GFX_CONFIG *GFX_Context_GetConfig(void); diff --git a/src/libtrx/include/libtrx/gfx/gl/program.h b/src/libtrx/include/libtrx/gfx/gl/program.h index fd6a2afb0..0bbfadc44 100644 --- a/src/libtrx/include/libtrx/gfx/gl/program.h +++ b/src/libtrx/include/libtrx/gfx/gl/program.h @@ -12,7 +12,7 @@ typedef struct { bool GFX_GL_Program_Init(GFX_GL_PROGRAM *program); void GFX_GL_Program_Close(GFX_GL_PROGRAM *program); -void GFX_GL_Program_Bind(GFX_GL_PROGRAM *program); +void GFX_GL_Program_Bind(const GFX_GL_PROGRAM *program); char *GFX_GL_Program_PreprocessShader( const char *content, GLenum type, GFX_GL_BACKEND backend); void GFX_GL_Program_AttachShader( diff --git a/src/libtrx/include/libtrx/gfx/renderer.h b/src/libtrx/include/libtrx/gfx/renderer.h index e7a461153..7fde3e69f 100644 --- a/src/libtrx/include/libtrx/gfx/renderer.h +++ b/src/libtrx/include/libtrx/gfx/renderer.h @@ -7,8 +7,5 @@ typedef struct GFX_Renderer { void (*shutdown)(struct GFX_Renderer *renderer); void (*reset)(struct GFX_Renderer *renderer); void (*swap_buffers)(struct GFX_Renderer *renderer); - void (*get_scale)( - const struct GFX_Renderer *renderer, float *out_scale_x, - float *out_scale_y); void *priv; } GFX_RENDERER; diff --git a/src/libtrx/include/libtrx/strings/common.h b/src/libtrx/include/libtrx/strings/common.h index 159f0d1a4..01e65d892 100644 --- a/src/libtrx/include/libtrx/strings/common.h +++ b/src/libtrx/include/libtrx/strings/common.h @@ -1,5 +1,6 @@ #pragma once +#include "../colors.h" #include "../vector.h" #include @@ -14,6 +15,7 @@ bool String_IsEmpty(const char *value); bool String_ParseBool(const char *value, bool *target); bool String_ParseInteger(const char *value, int32_t *target); bool String_ParseDecimal(const char *value, float *target); +bool String_ParseRGB888(const char *value, RGB_888 *target); char *String_ToUpper(const char *text); diff --git a/src/libtrx/include/libtrx/vector.h b/src/libtrx/include/libtrx/vector.h index 03f5ec475..ab4747ed3 100644 --- a/src/libtrx/include/libtrx/vector.h +++ b/src/libtrx/include/libtrx/vector.h @@ -15,6 +15,7 @@ typedef struct { VECTOR *Vector_Create(size_t item_size); VECTOR *Vector_CreateAtCapacity(size_t item_size, int32_t capacity); +void Vector_EnsureCapacity(VECTOR *vector, int32_t capacity); void Vector_Free(VECTOR *vector); int32_t Vector_IndexOf(const VECTOR *vector, const void *item); @@ -22,6 +23,7 @@ int32_t Vector_LastIndexOf(const VECTOR *vector, const void *item); bool Vector_Contains(const VECTOR *vector, const void *item); void *Vector_Get(VECTOR *vector, int32_t index); +void *Vector_GetData(VECTOR *vector); void Vector_Add(VECTOR *vector, const void *item); void Vector_Insert(VECTOR *vector, int32_t index, const void *item); void Vector_Swap(VECTOR *vector, int32_t index1, int32_t index2); diff --git a/src/libtrx/version.h b/src/libtrx/include/libtrx/version.h similarity index 100% rename from src/libtrx/version.h rename to src/libtrx/include/libtrx/version.h diff --git a/src/libtrx/meson.build b/src/libtrx/meson.build index b4cc7b9da..d3866bca2 100644 --- a/src/libtrx/meson.build +++ b/src/libtrx/meson.build @@ -136,6 +136,7 @@ sources = [ 'game/game_string_table/priv.c', 'game/game_string_table/reader.c', 'game/gun/common.c', + 'game/gym.c', 'game/inject/common.c', 'game/inject/data/anims.c', 'game/inject/data/meshes.c', @@ -163,16 +164,24 @@ sources = [ 'game/lara/misc.c', 'game/level/common.c', 'game/level/faces.c', + 'game/level/settings.c', 'game/math/trig.c', 'game/math/util.c', 'game/matrix.c', 'game/music.c', 'game/objects/common.c', - 'game/objects/names.c', 'game/objects/creatures/bear.c', 'game/objects/creatures/wolf.c', + 'game/objects/general/bridge_common.c', + 'game/objects/general/bridge_flat.c', + 'game/objects/general/bridge_tilt1.c', + 'game/objects/general/bridge_tilt2.c', + 'game/objects/general/drawbridge.c', + 'game/objects/general/trapdoor.c', + 'game/objects/names.c', 'game/objects/traps/movable_block.c', 'game/objects/vars.c', + 'game/output/background.c', 'game/output/common.c', 'game/output/textures.c', 'game/packer.c', @@ -189,22 +198,45 @@ sources = [ 'game/random.c', 'game/rooms/common.c', 'game/rooms/draw.c', - 'game/savegame.c', + 'game/savegame/common.c', + 'game/scaler.c', 'game/shell/common.c', 'game/shell/main.c', 'game/sound.c', 'game/text.c', 'game/ui/common.c', + 'game/ui/dialogs/base_passport.c', + 'game/ui/dialogs/controls.c', + 'game/ui/dialogs/controls_backend.c', + 'game/ui/dialogs/controls_editor.c', + 'game/ui/dialogs/examine_item.c', + 'game/ui/dialogs/new_game.c', + 'game/ui/dialogs/pause.c', + 'game/ui/dialogs/photo_mode.c', + 'game/ui/dialogs/play_any_level.c', + 'game/ui/dialogs/save_slot.c', + 'game/ui/dialogs/select_level.c', + 'game/ui/dialogs/stats.c', + 'game/ui/elements/anchor.c', + 'game/ui/elements/fade.c', + 'game/ui/elements/fixed.c', + 'game/ui/elements/flash.c', + 'game/ui/elements/frame.c', + 'game/ui/elements/hide.c', + 'game/ui/elements/label.c', + 'game/ui/elements/modal.c', + 'game/ui/elements/offset.c', + 'game/ui/elements/pad.c', + 'game/ui/elements/prompt.c', + 'game/ui/elements/requester.c', + 'game/ui/elements/resize.c', + 'game/ui/elements/spacer.c', + 'game/ui/elements/stack.c', + 'game/ui/elements/window.c', 'game/ui/events.c', - 'game/ui/widgets/console.c', - 'game/ui/widgets/frame.c', - 'game/ui/widgets/label.c', - 'game/ui/widgets/photo_mode.c', - 'game/ui/widgets/prompt.c', - 'game/ui/widgets/requester.c', - 'game/ui/widgets/spacer.c', - 'game/ui/widgets/stack.c', - 'game/ui/widgets/window.c', + 'game/ui/helpers.c', + 'game/ui/hud/console.c', + 'game/ui/hud/console_logs.c', 'gfx/2d/2d_renderer.c', 'gfx/2d/2d_surface.c', 'gfx/3d/3d_renderer.c', @@ -214,8 +246,8 @@ sources = [ 'gfx/gl/buffer.c', 'gfx/gl/program.c', 'gfx/gl/sampler.c', - 'gfx/gl/track.c', 'gfx/gl/texture.c', + 'gfx/gl/track.c', 'gfx/gl/utils.c', 'gfx/gl/vertex_array.c', 'gfx/renderers/fbo_renderer.c', diff --git a/src/libtrx/strings/common.c b/src/libtrx/strings/common.c index ffbd5e3cf..a3fad4f90 100644 --- a/src/libtrx/strings/common.c +++ b/src/libtrx/strings/common.c @@ -174,6 +174,16 @@ bool String_ParseDecimal(const char *const value, float *const target) return true; } +bool String_ParseRGB888(const char *value, RGB_888 *const target) +{ + if (value[0] == '#') { + value++; + } + return sscanf( + value, "%02hhX%02hhX%02hhX", &target->r, &target->g, &target->b) + == 3; +} + char *String_ToUpper(const char *text) { if (text == nullptr) { diff --git a/src/libtrx/vector.c b/src/libtrx/vector.c index 219eadc96..d4b31bf4e 100644 --- a/src/libtrx/vector.c +++ b/src/libtrx/vector.c @@ -42,6 +42,16 @@ VECTOR *Vector_CreateAtCapacity(const size_t item_size, const int32_t capacity) return vector; } +void Vector_EnsureCapacity(VECTOR *const vector, const int32_t capacity) +{ + if (vector->capacity >= capacity) { + return; + } + vector->capacity = capacity; + P(vector).items = + Memory_Realloc(P(vector).items, vector->item_size * capacity); +} + void Vector_Free(VECTOR *vector) { Memory_FreePointer(&P(vector).items); @@ -86,6 +96,11 @@ void *Vector_Get(VECTOR *const vector, const int32_t index) return (void *)(items + index * vector->item_size); } +void *Vector_GetData(VECTOR *const vector) +{ + return P(vector).items; +} + void Vector_Add(VECTOR *const vector, const void *const item) { M_EnsureCapacity(vector, 1); diff --git a/src/tr1/game/console/common.c b/src/tr1/game/console/common.c index f2b978358..9f3b2e4c5 100644 --- a/src/tr1/game/console/common.c +++ b/src/tr1/game/console/common.c @@ -11,7 +11,7 @@ void Console_DrawBackdrop(void) int32_t sw = Viewport_GetWidth(); int32_t sh = Screen_GetRenderScale( // not entirely accurate, but good enough - TEXT_HEIGHT * 1.0 + 10 * TEXT_HEIGHT * 0.8, RSR_TEXT); + TEXT_HEIGHT_FIXED * 1.0 + 7 * TEXT_HEIGHT_FIXED * 0.8, RSR_TEXT); int32_t sy = Viewport_GetHeight() - sh; RGBA_8888 top = { 0, 0, 0, 0 }; diff --git a/src/tr1/game/creature.c b/src/tr1/game/creature.c index af463525d..d5ee8ea3a 100644 --- a/src/tr1/game/creature.c +++ b/src/tr1/game/creature.c @@ -1,685 +1,27 @@ #include "game/creature.h" -#include "game/box.h" -#include "game/carrier.h" #include "game/effects.h" -#include "game/items.h" #include "game/lara/common.h" -#include "game/los.h" -#include "game/lot.h" -#include "game/objects/common.h" #include "game/objects/vars.h" #include "game/random.h" -#include "game/room.h" #include "game/spawn.h" -#include "global/vars.h" - -#include -#include -#include - -#define MAX_CREATURE_DISTANCE (WALL_L * 30) - -static bool M_SwitchToWater( - int16_t item_num, const int32_t *wh, const HYBRID_INFO *info); -static bool M_SwitchToLand( - int16_t item_num, const int32_t *wh, const HYBRID_INFO *info); -static bool M_TestSwitchOrKill(int16_t item_num, GAME_OBJECT_ID target_id); - -void Creature_Initialise(int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - - item->rot.y += (PHD_ANGLE)((Random_GetControl() - DEG_90) >> 1); - item->collidable = 1; - item->data = nullptr; -} - -void Creature_AIInfo(ITEM *item, AI_INFO *info) -{ - CREATURE *creature = item->data; - if (!creature) { - return; - } - - const int16_t *const zone = Box_GetLotZone(&creature->lot); - - const ROOM *room = Room_Get(item->room_num); - item->box_num = Room_GetWorldSector(room, item->pos.x, item->pos.z)->box; - info->zone_num = zone[item->box_num]; - - room = Room_Get(g_LaraItem->room_num); - g_LaraItem->box_num = - Room_GetWorldSector(room, g_LaraItem->pos.x, g_LaraItem->pos.z)->box; - info->enemy_zone = zone[g_LaraItem->box_num]; - - if (Box_GetBox(g_LaraItem->box_num)->overlap_index - & creature->lot.block_mask) { - info->enemy_zone |= BOX_BLOCKED; - } else if ( - creature->lot.node[item->box_num].search_num - == (creature->lot.search_num | BOX_BLOCKED_SEARCH)) { - info->enemy_zone |= BOX_BLOCKED; - } - - const OBJECT *const obj = Object_Get(item->object_id); - int32_t z = g_LaraItem->pos.z - - ((Math_Cos(item->rot.y) * obj->pivot_length) >> W2V_SHIFT) - - item->pos.z; - int32_t x = g_LaraItem->pos.x - - ((Math_Sin(item->rot.y) * obj->pivot_length) >> W2V_SHIFT) - - item->pos.x; - - PHD_ANGLE angle = Math_Atan(z, x); - info->distance = SQUARE(x) + SQUARE(z); - if (ABS(x) > MAX_CREATURE_DISTANCE || ABS(z) > MAX_CREATURE_DISTANCE) { - info->distance = SQUARE(MAX_CREATURE_DISTANCE); - } - info->angle = angle - item->rot.y; - info->enemy_facing = angle - g_LaraItem->rot.y + DEG_180; - info->ahead = info->angle > -FRONT_ARC && info->angle < FRONT_ARC; - info->bite = info->ahead && (g_LaraItem->pos.y > item->pos.y - STEP_L) - && (g_LaraItem->pos.y < item->pos.y + STEP_L); -} - -void Creature_Mood( - const ITEM *const item, const AI_INFO *const info, const bool violent) -{ - CREATURE *creature = item->data; - if (!creature) { - return; - } - - LOT_INFO *lot = &creature->lot; - const ITEM *const enemy = g_LaraItem; - if (lot->node[item->box_num].search_num - == (lot->search_num | BOX_BLOCKED_SEARCH)) { - lot->required_box = NO_BOX; - } - - if (creature->mood != MOOD_ATTACK && lot->required_box != NO_BOX - && !Box_ValidBox(item, info->zone_num, lot->target_box)) { - if (info->zone_num == info->enemy_zone) { - creature->mood = MOOD_BORED; - } - lot->required_box = NO_BOX; - } - - MOOD_TYPE mood = creature->mood; - - if (enemy->hit_points <= 0) { - creature->mood = MOOD_BORED; - } else if (violent) { - switch (mood) { - case MOOD_ATTACK: - if (info->zone_num != info->enemy_zone) { - creature->mood = MOOD_BORED; - } - break; - - case MOOD_BORED: - case MOOD_STALK: - if (info->zone_num == info->enemy_zone) { - creature->mood = MOOD_ATTACK; - } else if (item->hit_status) { - creature->mood = MOOD_ESCAPE; - } - break; - - case MOOD_ESCAPE: - if (info->zone_num == info->enemy_zone) { - creature->mood = MOOD_ATTACK; - } - break; - } - } else { - switch (mood) { - case MOOD_ATTACK: - if (item->hit_status - && (Random_GetControl() < ESCAPE_CHANCE - || info->zone_num != info->enemy_zone)) { - creature->mood = MOOD_ESCAPE; - } else if (info->zone_num != info->enemy_zone) { - creature->mood = MOOD_BORED; - } - break; - - case MOOD_BORED: - case MOOD_STALK: - if (item->hit_status - && (Random_GetControl() < ESCAPE_CHANCE - || info->zone_num != info->enemy_zone)) { - creature->mood = MOOD_ESCAPE; - } else if (info->zone_num == info->enemy_zone) { - if (info->distance < ATTACK_RANGE - || (creature->mood == MOOD_STALK - && lot->required_box == NO_BOX)) { - creature->mood = MOOD_ATTACK; - } else { - creature->mood = MOOD_STALK; - } - } - break; - - case MOOD_ESCAPE: - if (info->zone_num == info->enemy_zone - && Random_GetControl() < RECOVER_CHANCE) { - creature->mood = MOOD_STALK; - } - break; - } - } - - if (mood != creature->mood) { - if (mood == MOOD_ATTACK) { - Box_TargetBox(lot, lot->target_box); - } - lot->required_box = NO_BOX; - } - - switch (creature->mood) { - case MOOD_ATTACK: - if (Random_GetControl() < Object_Get(item->object_id)->smartness) { - lot->target.x = enemy->pos.x; - lot->target.y = enemy->pos.y; - lot->target.z = enemy->pos.z; - lot->required_box = enemy->box_num; - if (lot->fly && g_Lara.water_status == LWS_ABOVE_WATER) { - const ANIM_FRAME *const frame = Item_GetBestFrame(enemy); - lot->target.y += frame->bounds.min.y; - } - } - break; - - case MOOD_BORED: { - int box_num = - lot->node[Random_GetControl() * lot->zone_count / 0x7FFF].box_num; - if (Box_ValidBox(item, info->zone_num, box_num)) { - if (Box_StalkBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - creature->mood = MOOD_STALK; - } else if (lot->required_box == NO_BOX) { - Box_TargetBox(lot, box_num); - } - } - break; - } - - case MOOD_STALK: { - if (lot->required_box == NO_BOX - || !Box_StalkBox(item, enemy, lot->required_box)) { - int box_num = - lot->node[Random_GetControl() * lot->zone_count / 0x7FFF] - .box_num; - if (Box_ValidBox(item, info->zone_num, box_num)) { - if (Box_StalkBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - } else if (lot->required_box == NO_BOX) { - Box_TargetBox(lot, box_num); - if (info->zone_num != info->enemy_zone) { - creature->mood = MOOD_BORED; - } - } - } - } - break; - } - - case MOOD_ESCAPE: { - int box_num = - lot->node[Random_GetControl() * lot->zone_count / 0x7FFF].box_num; - if (Box_ValidBox(item, info->zone_num, box_num) - && lot->required_box == NO_BOX) { - if (Box_EscapeBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - } else if ( - info->zone_num == info->enemy_zone - && Box_StalkBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - creature->mood = MOOD_STALK; - } - } - break; - } - } - - if (lot->target_box == NO_BOX) { - Box_TargetBox(lot, item->box_num); - } - - Box_CalculateTarget(&creature->target, item, &creature->lot); -} - -int16_t Creature_Turn(ITEM *item, int16_t maximum_turn) -{ - CREATURE *creature = item->data; - if (!creature) { - return 0; - } - - if (!item->speed || !maximum_turn) { - return 0; - } - - int32_t x = creature->target.x - item->pos.x; - int32_t z = creature->target.z - item->pos.z; - int16_t angle = Math_Atan(z, x) - item->rot.y; - int32_t range = (item->speed << 14) / maximum_turn; - - if (angle > FRONT_ARC || angle < -FRONT_ARC) { - if (SQUARE(x) + SQUARE(z) < SQUARE(range)) { - maximum_turn >>= 1; - } - } - - if (angle > maximum_turn) { - angle = maximum_turn; - } else if (angle < -maximum_turn) { - angle = -maximum_turn; - } - - item->rot.y += angle; - - return angle; -} - -void Creature_Tilt(ITEM *item, int16_t angle) -{ - angle = angle * 4 - item->rot.z; - if (angle < -MAX_TILT) { - angle = -MAX_TILT; - } else if (angle > MAX_TILT) { - angle = MAX_TILT; - } - item->rot.z += angle; -} - -void Creature_Head(ITEM *item, int16_t required) -{ - CREATURE *creature = item->data; - if (!creature) { - return; - } - - int16_t change = required - creature->head_rotation; - if (change > MAX_HEAD_CHANGE) { - change = MAX_HEAD_CHANGE; - } else if (change < -MAX_HEAD_CHANGE) { - change = -MAX_HEAD_CHANGE; - } - - creature->head_rotation += change; - - if (creature->head_rotation > FRONT_ARC) { - creature->head_rotation = FRONT_ARC; - } else if (creature->head_rotation < -FRONT_ARC) { - creature->head_rotation = -FRONT_ARC; - } -} - -int16_t Creature_Effect( - const ITEM *const item, const BITE *const bite, - int16_t (*spawn)( - int32_t x, int32_t y, int32_t z, int16_t speed, int16_t yrot, - int16_t room_num)) -{ - XYZ_32 pos = { - .x = bite->x, - .y = bite->y, - .z = bite->z, - }; - Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); - return spawn(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num); -} - -bool Creature_CheckBaddieOverlap(int16_t item_num) -{ - const ITEM *item = Item_Get(item_num); - - int32_t x = item->pos.x; - int32_t y = item->pos.y; - int32_t z = item->pos.z; - const int32_t radius = SQUARE(Object_Get(item->object_id)->radius); - - int16_t link = Room_Get(item->room_num)->item_num; - do { - item = Item_Get(link); - - if (link == item_num) { - return false; - } - - if (item != g_LaraItem && item->status == IS_ACTIVE - && item->speed != 0) { - int32_t distance = SQUARE(item->pos.x - x) + SQUARE(item->pos.y - y) - + SQUARE(item->pos.z - z); - if (distance < radius) { - return true; - } - } - - link = item->next_item; - } while (link != NO_ITEM); - - return false; -} - -void Creature_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll) -{ - ITEM *const item = Item_Get(item_num); - - if (!Lara_TestBoundsCollide(item, coll->radius)) { - return; - } - if (!Collide_TestCollision(item, lara_item)) { - return; - } - - if (coll->enable_baddie_push) { - if (item->hit_points <= 0) { - Lara_Push(item, coll, 0, 0); - } else { - Lara_Push(item, coll, coll->enable_hit, 0); - } - } -} - -bool Creature_Animate(int16_t item_num, int16_t angle, int16_t tilt) -{ - ITEM *const item = Item_Get(item_num); - CREATURE *creature = item->data; - if (!creature) { - return false; - } - LOT_INFO *lot = &creature->lot; - - XYZ_32 old = { - .x = item->pos.x, - .y = item->pos.y, - .z = item->pos.z, - }; - - const int32_t box_height = Box_GetBox(item->box_num)->height; - - const int16_t *const zone = Box_GetLotZone(lot); - - Item_Animate(item); - if (item->status == IS_DEACTIVATED) { - item->collidable = 0; - item->hit_points = DONT_TARGET; - LOT_DisableBaddieAI(item_num); - Item_RemoveActive(item_num); - Carrier_TestItemDrops(item_num); - return false; - } - - const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); - int32_t y = item->pos.y + bounds->min.y; - - int16_t room_num = item->room_num; - const SECTOR *sector = - Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - int32_t height = Box_GetBox(sector->box)->height; - int16_t next_box = lot->node[sector->box].exit_box; - int32_t next_height; - if (next_box != NO_BOX) { - next_height = Box_GetBox(next_box)->height; - } else { - next_height = height; - } - - int32_t pos_x; - int32_t pos_z; - int32_t shift_x; - int32_t shift_z; - if (sector->box == NO_BOX || zone[item->box_num] != zone[sector->box] - || box_height - height > lot->step || box_height - height < lot->drop) { - pos_x = item->pos.x >> WALL_SHIFT; - - shift_x = old.x >> WALL_SHIFT; - shift_z = old.z >> WALL_SHIFT; - - if (pos_x < shift_x) { - item->pos.x = old.x & (~(WALL_L - 1)); - } else if (pos_x > shift_x) { - item->pos.x = old.x | (WALL_L - 1); - } - - if (pos_x < shift_z) { - item->pos.z = old.z & (~(WALL_L - 1)); - } else if (pos_x > shift_z) { - item->pos.z = old.z | (WALL_L - 1); - } - - sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - height = Box_GetBox(sector->box)->height; - next_box = lot->node[sector->box].exit_box; - if (next_box != NO_BOX) { - next_height = Box_GetBox(next_box)->height; - } else { - next_height = height; - } - } - - int32_t x = item->pos.x; - int32_t z = item->pos.z; - - pos_x = x & (WALL_L - 1); - pos_z = z & (WALL_L - 1); - shift_x = 0; - shift_z = 0; - - const int32_t radius = Object_Get(item->object_id)->radius; - - if (pos_z < radius) { - if (Box_BadFloor( - x, y, z - radius, height, next_height, room_num, lot)) { - shift_z = radius - pos_z; - } - - if (pos_x < radius) { - if (Box_BadFloor( - x - radius, y, z, height, next_height, room_num, lot)) { - shift_x = radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x - radius, y, z - radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { - shift_z = radius - pos_z; - } else { - shift_x = radius - pos_x; - } - } - } else if (pos_x > WALL_L - radius) { - if (Box_BadFloor( - x + radius, y, z, height, next_height, room_num, lot)) { - shift_x = WALL_L - radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x + radius, y, z - radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { - shift_z = radius - pos_z; - } else { - shift_x = WALL_L - radius - pos_x; - } - } - } - } else if (pos_z > WALL_L - radius) { - if (Box_BadFloor( - x, y, z + radius, height, next_height, room_num, lot)) { - shift_z = WALL_L - radius - pos_z; - } - - if (pos_x < radius) { - if (Box_BadFloor( - x - radius, y, z, height, next_height, room_num, lot)) { - shift_x = radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x - radius, y, z + radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { - shift_x = radius - pos_x; - } else { - shift_z = WALL_L - radius - pos_z; - } - } - } else if (pos_x > WALL_L - radius) { - if (Box_BadFloor( - x + radius, y, z, height, next_height, room_num, lot)) { - shift_x = WALL_L - radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x + radius, y, z + radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { - shift_x = WALL_L - radius - pos_x; - } else { - shift_z = WALL_L - radius - pos_z; - } - } - } - } else if (pos_x < radius) { - if (Box_BadFloor( - x - radius, y, z, height, next_height, room_num, lot)) { - shift_x = radius - pos_x; - } - } else if (pos_x > WALL_L - radius) { - if (Box_BadFloor( - x + radius, y, z, height, next_height, room_num, lot)) { - shift_x = WALL_L - radius - pos_x; - } - } - - item->pos.x += shift_x; - item->pos.z += shift_z; - - if (shift_x || shift_z) { - sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - - item->rot.y += angle; - Creature_Tilt(item, tilt * 2); - } - - if (Creature_CheckBaddieOverlap(item_num)) { - item->pos.x = old.x; - item->pos.y = old.y; - item->pos.z = old.z; - return true; - } - - if (lot->fly) { - int32_t dy = creature->target.y - item->pos.y; - if (dy > lot->fly) { - dy = lot->fly; - } else if (dy < -lot->fly) { - dy = -lot->fly; - } - - height = Room_GetHeight(sector, item->pos.x, y, item->pos.z); - if (item->pos.y + dy > height) { - if (item->pos.y > height) { - item->pos.x = old.x; - item->pos.z = old.z; - dy = -lot->fly; - } else { - dy = 0; - item->pos.y = height; - } - } else { - int32_t ceiling = - Room_GetCeiling(sector, item->pos.x, y, item->pos.z); - - int32_t min_y = item->object_id == O_ALLIGATOR ? 0 : bounds->min.y; - if (item->pos.y + min_y + dy < ceiling) { - if (item->pos.y + min_y < ceiling) { - item->pos.x = old.x; - item->pos.z = old.z; - dy = lot->fly; - } else { - dy = 0; - } - } - } - - item->pos.y += dy; - sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - item->floor = Room_GetHeight(sector, item->pos.x, y, item->pos.z); - - angle = item->speed ? Math_Atan(item->speed, -dy) : 0; - if (angle < item->rot.x - DEG_1) { - item->rot.x -= DEG_1; - } else if (angle > item->rot.x + DEG_1) { - item->rot.x += DEG_1; - } else { - item->rot.x = angle; - } - } else { - sector = - Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); - item->floor = - Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); - - if (item->pos.y > item->floor) { - item->pos.y = item->floor; - } else if (item->floor - item->pos.y > STEP_L / 4) { - item->pos.y += STEP_L / 4; - } else if (item->pos.y < item->floor) { - item->pos.y = item->floor; - } - - item->rot.x = 0; - } - - if (item->room_num != room_num) { - Item_NewRoom(item_num, room_num); - } - - return true; -} - -bool Creature_CanTargetEnemy(ITEM *item, AI_INFO *info) -{ - if (!info->ahead || info->distance >= CREATURE_SHOOT_RANGE) { - return false; - } - - GAME_VECTOR start; - start.x = item->pos.x; - start.y = item->pos.y - STEP_L * 3; - start.z = item->pos.z; - start.room_num = item->room_num; - - GAME_VECTOR target; - target.x = g_LaraItem->pos.x; - target.y = g_LaraItem->pos.y - STEP_L * 3; - target.z = g_LaraItem->pos.z; - - return LOS_Check(&start, &target); -} bool Creature_ShootAtLara( ITEM *item, int32_t distance, BITE *gun, int16_t extra_rotation, int16_t damage) { - bool hit; + bool is_hit; if (distance > CREATURE_SHOOT_RANGE) { - hit = false; + is_hit = false; } else { - hit = Random_GetControl() + is_hit = Random_GetControl() < ((CREATURE_SHOOT_RANGE - distance) / (CREATURE_SHOOT_RANGE / 0x7FFF) - CREATURE_MISS_CHANCE); } int16_t effect_num; - if (hit) { + if (is_hit) { effect_num = Creature_Effect(item, gun, Spawn_GunShotHit); } else { effect_num = Creature_Effect(item, gun, Spawn_GunShotMiss); @@ -689,25 +31,11 @@ bool Creature_ShootAtLara( Effect_Get(effect_num)->rot.y += extra_rotation; } - if (hit) { + if (is_hit) { Lara_TakeDamage(damage, true); } - return hit; -} - -bool Creature_EnsureHabitat( - const int16_t item_num, int32_t *const wh, const HYBRID_INFO *const info) -{ - // Test the environment for a hybrid creature. Record the water height and - // return whether or not a type conversion has taken place. - const ITEM *const item = Item_Get(item_num); - *wh = Room_GetWaterHeight( - item->pos.x, item->pos.y, item->pos.z, item->room_num); - - return item->object_id == info->land.id - ? M_SwitchToWater(item_num, wh, info) - : M_SwitchToLand(item_num, wh, info); + return is_hit; } bool Creature_IsBoss(const int16_t item_num) @@ -715,100 +43,3 @@ bool Creature_IsBoss(const int16_t item_num) const ITEM *const item = Item_Get(item_num); return Object_IsType(item->object_id, g_BossObjects); } - -static bool M_SwitchToWater( - const int16_t item_num, const int32_t *const wh, - const HYBRID_INFO *const info) -{ - if (*wh == NO_HEIGHT) { - return false; - } - - ITEM *const item = Item_Get(item_num); - - if (item->hit_points <= 0) { - // Dead land creatures should remain in their pose permanently. - return false; - } - - // The land creature is alive and the room has been flooded. Switch to the - // water creature. - if (!M_TestSwitchOrKill(item_num, info->water.id)) { - return false; - } - - item->object_id = info->water.id; - Item_SwitchToAnim(item, info->water.active_anim, 0); - item->current_anim_state = Item_GetAnim(item)->current_anim_state; - item->goal_anim_state = item->current_anim_state; - item->pos.y = *wh; - - return true; -} - -static bool M_SwitchToLand( - const int16_t item_num, const int32_t *const wh, - const HYBRID_INFO *const info) -{ - if (*wh != NO_HEIGHT) { - return false; - } - - if (!M_TestSwitchOrKill(item_num, info->land.id)) { - return false; - } - - ITEM *const item = Item_Get(item_num); - - // Switch to the land creature regardless of death state. - item->object_id = info->land.id; - item->rot.x = 0; - - if (item->hit_points > 0) { - Item_SwitchToAnim(item, info->land.active_anim, 0); - item->current_anim_state = Item_GetAnim(item)->current_anim_state; - item->goal_anim_state = item->current_anim_state; - - } else { - Item_SwitchToAnim(item, info->land.death_anim, -1); - item->current_anim_state = info->land.death_state; - item->goal_anim_state = item->current_anim_state; - - int16_t room_num = item->room_num; - const SECTOR *const sector = - Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); - item->floor = - Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); - item->pos.y = item->floor; - - if (item->room_num != room_num) { - Item_NewRoom(item_num, room_num); - } - } - - return true; -} - -static bool M_TestSwitchOrKill( - const int16_t item_num, const GAME_OBJECT_ID target_id) -{ - if (Object_Get(target_id)->loaded) { - return true; - } - - LOG_WARNING( - "Object %d is not loaded; item %d cannot be converted.", target_id, - item_num); - Item_Kill(item_num); - return false; -} - -bool Creature_IsHostile(const ITEM *const item) -{ - return Object_IsType(item->object_id, g_EnemyObjects); -} - -bool Creature_IsAlly(const ITEM *const item) -{ - return Object_IsType(item->object_id, g_AllyObjects); -} diff --git a/src/tr1/game/creature.h b/src/tr1/game/creature.h index 9c581565e..68c2c50cb 100644 --- a/src/tr1/game/creature.h +++ b/src/tr1/game/creature.h @@ -1,32 +1,8 @@ #pragma once -#include "global/const.h" -#include "global/types.h" - #include -#include -#define CREATURE_SHOOT_RANGE SQUARE(WALL_L * 7) // = 51380224 -#define CREATURE_MISS_CHANCE 0x2000 - -typedef struct { - struct { - GAME_OBJECT_ID id; - int16_t active_anim; - int16_t death_anim; - int16_t death_state; - } land; - struct { - GAME_OBJECT_ID id; - int16_t active_anim; - } water; -} HYBRID_INFO; - -bool Creature_CheckBaddieOverlap(int16_t item_num); -bool Creature_CanTargetEnemy(ITEM *item, AI_INFO *info); +bool Creature_IsBoss(int16_t item_num); bool Creature_ShootAtLara( ITEM *item, int32_t distance, BITE *gun, int16_t extra_rotation, int16_t damage); -bool Creature_EnsureHabitat( - int16_t item_num, int32_t *wh, const HYBRID_INFO *info); -bool Creature_IsBoss(int16_t item_num); diff --git a/src/tr1/game/effects.c b/src/tr1/game/effects.c index 9c9446577..ac0ff9d67 100644 --- a/src/tr1/game/effects.c +++ b/src/tr1/game/effects.c @@ -1,5 +1,6 @@ #include "game/effects.h" +#include "game/objects/vars.h" #include "game/output.h" #include "game/room.h" #include "global/const.h" @@ -15,15 +16,15 @@ static int16_t m_NextEffectFree = NO_EFFECT; void Effect_InitialiseArray(void) { - m_Effects = GameBuf_Alloc(NUM_EFFECTS * sizeof(EFFECT), GBUF_EFFECTS); + m_Effects = GameBuf_Alloc(MAX_EFFECTS * sizeof(EFFECT), GBUF_EFFECTS); m_NextEffectActive = NO_EFFECT; m_NextEffectFree = 0; - for (int i = 0; i < NUM_EFFECTS - 1; i++) { + for (int32_t i = 0; i < MAX_EFFECTS - 1; i++) { m_Effects[i].next_draw = i + 1; m_Effects[i].next_free = i + 1; } - m_Effects[NUM_EFFECTS - 1].next_draw = NO_EFFECT; - m_Effects[NUM_EFFECTS - 1].next_free = NO_EFFECT; + m_Effects[MAX_EFFECTS - 1].next_draw = NO_EFFECT; + m_Effects[MAX_EFFECTS - 1].next_free = NO_EFFECT; } void Effect_Control(void) @@ -144,10 +145,14 @@ void Effect_Draw(const int16_t effect_num) } if (obj->mesh_count < 0) { + const RGB_F tint = + Object_IsType(effect->object_id, g_WaterSpriteObjects) + ? (RGB_F) { 1.0f, 1.0f, 1.0f } + : Output_GetTint(); Output_DrawSprite( effect->interp.result.pos.x, effect->interp.result.pos.y, effect->interp.result.pos.z, obj->mesh_idx - effect->frame_num, - 4096); + SHADE_NEUTRAL, tint); } else { Matrix_Push(); Matrix_TranslateAbs32(effect->interp.result.pos); diff --git a/src/tr1/game/fmv.c b/src/tr1/game/fmv.c index 7474c6bf3..595b7e863 100644 --- a/src/tr1/game/fmv.c +++ b/src/tr1/game/fmv.c @@ -2,11 +2,11 @@ #include "game/input.h" #include "game/music.h" +#include "game/output.h" #include "game/screen.h" #include "game/shell.h" #include "game/sound.h" #include "global/types.h" -#include "specific/s_output.h" #include "specific/s_shell.h" #include @@ -58,13 +58,12 @@ static void M_ClearSurface(void *const surface, void *const user_data) static void M_RenderBegin(void *surface, void *const user_data) { - S_Output_RenderBegin(); + Output_BeginScene(); } static void M_RenderEnd(void *surface, void *const user_data) { - S_Output_RenderEnd(); - S_Output_FlipScreen(); + Output_EndScene(); } static void *M_LockSurface(void *const surface, void *const user_data) @@ -130,7 +129,7 @@ static bool M_Play(const char *const file_path) Audio_Init(); GFX_2D_Renderer_Destroy(renderer_2d); - S_Output_ApplyRenderSettings(); + Output_ApplyRenderSettings(); return true; } diff --git a/src/tr1/game/game/game.c b/src/tr1/game/game/game.c index e8e4371d9..8a9488eb1 100644 --- a/src/tr1/game/game/game.c +++ b/src/tr1/game/game/game.c @@ -23,7 +23,7 @@ #include #include #include -#include +#include #define FRAME_BUFFER(key) \ do { \ @@ -48,10 +48,11 @@ void Game_ProcessInput(void) } } - if (g_InputDB.use_small_medi && Inv_RequestItem(O_MEDI_OPTION)) { - Lara_UseItem(O_MEDI_OPTION); - } else if (g_InputDB.use_big_medi && Inv_RequestItem(O_BIGMEDI_OPTION)) { - Lara_UseItem(O_BIGMEDI_OPTION); + if (g_InputDB.use_small_medi && Inv_RequestItem(O_SMALL_MEDIPACK_OPTION)) { + Lara_UseItem(O_SMALL_MEDIPACK_OPTION); + } else if ( + g_InputDB.use_big_medi && Inv_RequestItem(O_LARGE_MEDIPACK_OPTION)) { + Lara_UseItem(O_LARGE_MEDIPACK_OPTION); } if (g_Config.input.enable_buffering && Game_IsPlaying()) { diff --git a/src/tr1/game/game/game_draw.c b/src/tr1/game/game/game_draw.c index c09917042..63530b1fa 100644 --- a/src/tr1/game/game/game_draw.c +++ b/src/tr1/game/game/game_draw.c @@ -4,6 +4,7 @@ #include "game/lara/draw.h" #include "game/lara/hair.h" #include "game/output.h" +#include "game/output/textures.h" #include "game/overlay.h" #include "game/room_draw.h" #include "game/viewport.h" @@ -24,7 +25,7 @@ void Game_Draw(bool draw_overlay) Room_DrawAllRooms(g_Camera.interp.room_num, g_Camera.target.room_num); if (g_Config.visuals.enable_reflections) { - Output_FillEnvironmentMap(); + Output_Textures_UpdateEnvironmentMap(); } if (Room_Get(g_LaraItem->room_num)->flags & RF_UNDERWATER) { diff --git a/src/tr1/game/game_flow/sequencer.h b/src/tr1/game/game_flow/sequencer.h index 6f45e734f..ce83afff5 100644 --- a/src/tr1/game/game_flow/sequencer.h +++ b/src/tr1/game/game_flow/sequencer.h @@ -6,4 +6,5 @@ void GF_InitSequencer(void); GF_COMMAND GF_DoLevelSequence( const GF_LEVEL *start_level, GF_SEQUENCE_CONTEXT seq_ctx); + GF_COMMAND GF_PlayAvailableStory(int32_t slot_num); diff --git a/src/tr1/game/game_flow/sequencer_events.c b/src/tr1/game/game_flow/sequencer_events.c index 373643335..9d786ed98 100644 --- a/src/tr1/game/game_flow/sequencer_events.c +++ b/src/tr1/game/game_flow/sequencer_events.c @@ -88,8 +88,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) // select level feature Savegame_InitCurrentInfo(); if (level->num > GF_GetFirstLevel()->num) { - Savegame_LoadOnlyResumeInfo( - g_GameInfo.select_save_slot, &g_GameInfo); + Savegame_LoadOnlyResumeInfo(g_GameInfo.select_save_slot); const GF_LEVEL *tmp_level = level; while (tmp_level != nullptr) { Savegame_ResetCurrentInfo(tmp_level); @@ -219,7 +218,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleLevelComplete) g_Config.profile.new_game_plus_unlock = true; Config_Write(); } - g_GameInfo.bonus_level_unlock = Stats_CheckAllSecretsCollected(GFL_NORMAL); + const bool bonus_level_unlock = Stats_CheckAllSecretsCollected(GFL_NORMAL); // play specific level if (g_GameInfo.select_level_num != -1) { @@ -242,7 +241,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleLevelComplete) Savegame_CarryCurrentInfoToNextLevel(current_level, next_level); Savegame_ApplyLogicToCurrentInfo(next_level); - if (next_level->type == GFL_BONUS && !g_GameInfo.bonus_level_unlock) { + if (next_level->type == GFL_BONUS && !bonus_level_unlock) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } return (GF_COMMAND) { @@ -307,7 +306,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleAddItem) static DECLARE_GF_EVENT_HANDLER(M_HandleRemoveWeapons) { if (seq_ctx != GFSC_STORY && seq_ctx != GFSC_SAVED - && !(g_GameInfo.bonus_flag & GBF_NGPLUS)) { + && !Game_IsBonusFlagSet(GBF_NGPLUS)) { g_GameInfo.remove_guns = true; } return (GF_COMMAND) { .action = GF_NOOP }; @@ -316,7 +315,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleRemoveWeapons) static DECLARE_GF_EVENT_HANDLER(M_HandleRemoveAmmo) { if (seq_ctx != GFSC_STORY && seq_ctx != GFSC_SAVED - && !(g_GameInfo.bonus_flag & GBF_NGPLUS)) { + && !Game_IsBonusFlagSet(GBF_NGPLUS)) { g_GameInfo.remove_ammo = true; } return (GF_COMMAND) { .action = GF_NOOP }; @@ -369,7 +368,7 @@ void GF_PreSequenceHook( g_GameInfo.remove_ammo = false; g_GameInfo.remove_medipacks = false; if (seq_ctx == GFSC_SAVED) { - g_GameInfo.bonus_flag = false; + Game_SetBonusFlag(GBF_NONE); } } @@ -389,25 +388,6 @@ GF_SEQUENCE_CONTEXT GF_SwitchSequenceContext( } } -bool GF_ShouldSkipSequenceEvent( - const GF_LEVEL *const level, const GF_SEQUENCE_EVENT *const event) -{ - // Skip cinematic levels - if (!g_Config.gameplay.enable_cine && level->type == GFL_CUTSCENE) { - switch (event->type) { - case GFS_EXIT_TO_TITLE: - case GFS_LEVEL_COMPLETE: - case GFS_PLAY_FMV: - case GFS_LEVEL_STATS: - case GFS_TOTAL_STATS: - return false; - default: - return true; - } - } - return false; -} - GF_EVENT_QUEUE_TYPE GF_ShouldDeferSequenceEvent( const GF_SEQUENCE_EVENT_TYPE event_type) { diff --git a/src/tr1/game/game_flow/sequencer_misc.c b/src/tr1/game/game_flow/sequencer_misc.c index b43cfb0aa..a3d7068ab 100644 --- a/src/tr1/game/game_flow/sequencer_misc.c +++ b/src/tr1/game/game_flow/sequencer_misc.c @@ -6,8 +6,10 @@ #include "game/level.h" #include "game/savegame.h" +#include #include #include +#include GF_COMMAND GF_RunTitle(void) { @@ -23,6 +25,12 @@ GF_COMMAND GF_RunTitle(void) GF_COMMAND GF_PlayAvailableStory(const int32_t slot_num) { const int32_t savegame_level = Savegame_GetLevelNumber(slot_num); + const bool prev_enable_legal = g_Config.gameplay.enable_legal; + g_Config.gameplay.enable_legal = false; + + // Play intro FMVs and cutscenes + GF_DoFrontendSequence(); + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i <= MIN(savegame_level, level_table->count); i++) { const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); @@ -36,9 +44,64 @@ GF_COMMAND GF_PlayAvailableStory(const int32_t slot_num) break; } } + + g_Config.gameplay.enable_legal = prev_enable_legal; return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } +bool GF_HasAvailableStory(const int32_t slot_num) +{ + const int32_t savegame_level = Savegame_GetLevelNumber(slot_num); + + // Check intro FMVs and cutscenes in frontend sequence (skip legal FMVs) + const GF_LEVEL *const title_level = GF_GetTitleLevel(); + if (title_level != nullptr) { + const GF_SEQUENCE *const seq = &title_level->sequence; + for (int32_t j = 0; j < seq->length; j++) { + const GF_SEQUENCE_EVENT *const ev = &seq->events[j]; + if (ev->type == GFS_PLAY_CUTSCENE) { + return true; + } + if (ev->type == GFS_PLAY_FMV) { + const int32_t fmv_id = (int32_t)(intptr_t)ev->data; + const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id]; + if (!fmv->is_legal) { + return true; + } + } + } + } + + // Check for any cutscenes or FMVs up until the save point + const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); + const int32_t max_level = MIN(savegame_level, level_table->count); + for (int32_t i = 0; i <= max_level; i++) { + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + if (level->type == GFL_GYM) { + continue; + } + const GF_SEQUENCE *const seq = &level->sequence; + for (int32_t j = 0; j < seq->length; j++) { + const GF_SEQUENCE_EVENT *const ev = &seq->events[j]; + // Stop checking after the saved level + if (ev->type == GFS_LOOP_GAME) { + break; + } + if (ev->type == GFS_PLAY_CUTSCENE) { + return true; + } + if (ev->type == GFS_PLAY_FMV) { + const int32_t fmv_id = (int32_t)(intptr_t)ev->data; + const GF_FMV *const fmv = &g_GameFlow.fmvs[fmv_id]; + if (!fmv->is_legal) { + return true; + } + } + } + } + return false; +} + GF_COMMAND GF_DoLevelSequence( const GF_LEVEL *const start_level, const GF_SEQUENCE_CONTEXT seq_ctx) { diff --git a/src/tr1/game/game_string.c b/src/tr1/game/game_string.c index 56bf3122b..d30284e31 100644 --- a/src/tr1/game/game_string.c +++ b/src/tr1/game/game_string.c @@ -4,9 +4,9 @@ void GameString_Init(void) { -#include "game_string.def" - #include +// force order +#include "game_string.def" } void GameString_Shutdown(void) diff --git a/src/tr1/game/game_string.def b/src/tr1/game/game_string.def index 99d41e841..4b0cffd37 100644 --- a/src/tr1/game/game_string.def +++ b/src/tr1/game/game_string.def @@ -1,34 +1,15 @@ GS_DEFINE(PASSPORT_RESTART_LEVEL, "Restart Level") -GS_DEFINE(PASSPORT_STORY_SO_FAR, "Story so far...") -GS_DEFINE(PASSPORT_LEGACY_SELECT_LEVEL_1, "Legacy saves do not") -GS_DEFINE(PASSPORT_LEGACY_SELECT_LEVEL_2, "support this feature.") -GS_DEFINE(PASSPORT_SELECT_MODE, "Select Mode") -GS_DEFINE(PASSPORT_MODE_NEW_GAME, "New Game") -GS_DEFINE(PASSPORT_MODE_NEW_GAME_PLUS, "New Game+") -GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP, "Japanese NG") -GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP_PLUS, "Japanese NG+") -GS_DEFINE(DETAIL_SELECT_DETAIL, "Select Detail") -GS_DEFINE(DETAIL_FPS, "FPS") -GS_DEFINE(DETAIL_TRAPEZOID_FILTER, "Trapezoid filter") -GS_DEFINE(DETAIL_PRETTY_PIXELS, "Pretty pixels") GS_DEFINE(DETAIL_REFLECTIONS, "Reflections") -GS_DEFINE(DETAIL_BILINEAR, "Bilinear") -GS_DEFINE(DETAIL_TEXTURE_FILTER, "Texture filter") GS_DEFINE(DETAIL_FBO_FILTER, "FBO filter") GS_DEFINE(DETAIL_VSYNC, "VSync") GS_DEFINE(DETAIL_BRIGHTNESS, "Brightness") -GS_DEFINE(DETAIL_UI_TEXT_SCALE, "UI text scale") -GS_DEFINE(DETAIL_UI_BAR_SCALE, "UI bar scale") -GS_DEFINE(DETAIL_RENDER_MODE, "Render mode") GS_DEFINE(DETAIL_RENDER_MODE_LEGACY, "Window size") GS_DEFINE(DETAIL_RENDER_MODE_FBO, "Framebuffer") GS_DEFINE(DETAIL_RESOLUTION, "Resolution") -GS_DEFINE(DETAIL_DECIMAL_FMT, "%d") GS_DEFINE(DETAIL_STRING_FMT, "%s") -GS_DEFINE(DETAIL_FLOAT_FMT, "%.1f") GS_DEFINE(DETAIL_RESOLUTION_FMT, "%dx%d") -GS_DEFINE(CONTROL_RESET_DEFAULTS, "Reset All: Hold %s") -GS_DEFINE(CONTROL_UNBIND, "Unbind: Hold %s") +GS_DEFINE(CONTROLS_RESET_DEFAULTS, "Reset All: Hold %s") +GS_DEFINE(CONTROLS_UNBIND, "Unbind: Hold %s") GS_DEFINE(KEYMAP_CHANGE_TARGET, "Change Target") GS_DEFINE(KEYMAP_EQUIP_PISTOLS, "Equip Pistols") GS_DEFINE(KEYMAP_EQUIP_SHOTGUN, "Equip Shotgun") @@ -49,7 +30,6 @@ GS_DEFINE(STATS_BONUS_STATISTICS, "Bonus Statistics") GS_DEFINE(STATS_AMMO, "AMMO HITS/USED") GS_DEFINE(STATS_DISTANCE_TRAVELLED, "DISTANCE TRAVELLED") GS_DEFINE(STATS_MEDIPACKS_USED, "HEALTH PACKS USED") -GS_DEFINE(MISC_EMPTY_SLOT_FMT, "- EMPTY SLOT %d -") GS_DEFINE(OSD_FLY_MODE_ON, "Fly mode enabled") GS_DEFINE(OSD_FLY_MODE_OFF, "Fly mode disabled") GS_DEFINE(OSD_GIVE_ITEM_ALL_KEYS, "Surprise! Every key item Lara needs is now in her backpack.") @@ -66,4 +46,4 @@ GS_DEFINE(OSD_DOOR_CLOSE, "Close Sesame!") GS_DEFINE(OSD_DOOR_OPEN_FAIL, "No doors in Lara's proximity") GS_DEFINE(ITEM_EXAMINE_ROLE, "\\{button empty} %s: Examine") GS_DEFINE(ITEM_USE_ROLE, "\\{button empty} %s: Use") -GS_DEFINE(PAGINATION_NAV, "%d / %d") +GS_DEFINE(MISC_EMPTY_SLOT_FMT, "- EMPTY SLOT %d -") diff --git a/src/tr1/game/gun/gun.c b/src/tr1/game/gun/gun.c index 623328f7d..ac82f2ea1 100644 --- a/src/tr1/game/gun/gun.c +++ b/src/tr1/game/gun/gun.c @@ -137,7 +137,7 @@ void Gun_Control(void) switch (g_Lara.gun_type) { case LGT_PISTOLS: - if (g_Lara.pistols.ammo && g_Input.action) { + if (g_Lara.pistol_ammo.ammo && g_Input.action) { Lara_SwapSingleMesh(LM_HEAD, O_UZI_ANIM); } if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { @@ -147,7 +147,7 @@ void Gun_Control(void) break; case LGT_MAGNUMS: - if (g_Lara.magnums.ammo && g_Input.action) { + if (g_Lara.magnum_ammo.ammo && g_Input.action) { Lara_SwapSingleMesh(LM_HEAD, O_UZI_ANIM); } if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { @@ -157,7 +157,7 @@ void Gun_Control(void) break; case LGT_UZIS: - if (g_Lara.uzis.ammo && g_Input.action) { + if (g_Lara.uzi_ammo.ammo && g_Input.action) { Lara_SwapSingleMesh(LM_HEAD, O_UZI_ANIM); } if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { @@ -167,7 +167,7 @@ void Gun_Control(void) break; case LGT_SHOTGUN: - if (g_Lara.shotgun.ammo && g_Input.action) { + if (g_Lara.shotgun_ammo.ammo && g_Input.action) { Lara_SwapSingleMesh(LM_HEAD, O_UZI_ANIM); } if (g_Camera.type != CAM_CINEMATIC && g_Camera.type != CAM_LOOK) { diff --git a/src/tr1/game/gun/gun_misc.c b/src/tr1/game/gun/gun_misc.c index da94c7698..596df9a5a 100644 --- a/src/tr1/game/gun/gun_misc.c +++ b/src/tr1/game/gun/gun_misc.c @@ -49,8 +49,8 @@ #define SHOTGUN_RARM_XMIN (-65 * DEG_1) #define SHOTGUN_RARM_XMAX (+65 * DEG_1) -static ITEM *m_TargetList[NUM_SLOTS]; -static ITEM *m_LastTargetList[NUM_SLOTS]; +static ITEM *m_TargetList[LOT_SLOT_COUNT]; +static ITEM *m_LastTargetList[LOT_SLOT_COUNT]; WEAPON_INFO g_Weapons[NUM_WEAPONS] = { // null @@ -269,7 +269,7 @@ void Gun_GetNewTarget(WEAPON_INFO *winfo) } if (num_targets > 0) { - for (int slot = 0; slot < NUM_SLOTS; slot++) { + for (int slot = 0; slot < LOT_SLOT_COUNT; slot++) { if (!m_TargetList[slot]) { g_Lara.target = nullptr; } @@ -288,7 +288,7 @@ void Gun_GetNewTarget(WEAPON_INFO *winfo) } if (g_Lara.target != m_LastTargetList[0]) { - for (int slot = NUM_SLOTS - 1; slot > 0; slot--) { + for (int slot = LOT_SLOT_COUNT - 1; slot > 0; slot--) { m_LastTargetList[slot] = m_LastTargetList[slot - 1]; } m_LastTargetList[0] = g_Lara.target; @@ -302,12 +302,12 @@ void Gun_ChangeTarget(WEAPON_INFO *winfo) g_Lara.target = nullptr; bool found_new_target = false; - for (int new_target = 0; new_target < NUM_SLOTS; new_target++) { + for (int new_target = 0; new_target < LOT_SLOT_COUNT; new_target++) { if (!m_TargetList[new_target]) { break; } - for (int last_target = 0; last_target < NUM_SLOTS; last_target++) { + for (int last_target = 0; last_target < LOT_SLOT_COUNT; last_target++) { if (!m_LastTargetList[last_target]) { found_new_target = true; break; @@ -325,7 +325,8 @@ void Gun_ChangeTarget(WEAPON_INFO *winfo) } if (g_Lara.target != m_LastTargetList[0]) { - for (int last_target = NUM_SLOTS - 1; last_target > 0; last_target--) { + for (int last_target = LOT_SLOT_COUNT - 1; last_target > 0; + last_target--) { m_LastTargetList[last_target] = m_LastTargetList[last_target - 1]; } m_LastTargetList[0] = g_Lara.target; @@ -394,28 +395,28 @@ int32_t Gun_FireWeapon( AMMO_INFO *ammo; switch (weapon_type) { case LGT_MAGNUMS: - ammo = &g_Lara.magnums; - if (g_GameInfo.bonus_flag & GBF_NGPLUS) { + ammo = &g_Lara.magnum_ammo; + if (Game_IsBonusFlagSet(GBF_NGPLUS)) { ammo->ammo = 1000; } break; case LGT_UZIS: - ammo = &g_Lara.uzis; - if (g_GameInfo.bonus_flag & GBF_NGPLUS) { + ammo = &g_Lara.uzi_ammo; + if (Game_IsBonusFlagSet(GBF_NGPLUS)) { ammo->ammo = 1000; } break; case LGT_SHOTGUN: - ammo = &g_Lara.shotgun; - if (g_GameInfo.bonus_flag & GBF_NGPLUS) { + ammo = &g_Lara.shotgun_ammo; + if (Game_IsBonusFlagSet(GBF_NGPLUS)) { ammo->ammo = 1000; } break; default: - ammo = &g_Lara.pistols; + ammo = &g_Lara.pistol_ammo; ammo->ammo = 1000; break; } @@ -478,7 +479,7 @@ int32_t Gun_FireWeapon( vdest.z = vsrc.z + ((bestdist * g_MatrixPtr->_22) >> W2V_SHIFT); Gun_HitTarget( target, &vdest, - winfo->damage * (g_GameInfo.bonus_flag & GBF_JAPANESE ? 2 : 1)); + winfo->damage * (Game_IsBonusFlagSet(GBF_JAPANESE) ? 2 : 1)); return 1; } diff --git a/src/tr1/game/gun/gun_rifle.c b/src/tr1/game/gun/gun_rifle.c index b38a81612..474b335f4 100644 --- a/src/tr1/game/gun/gun_rifle.c +++ b/src/tr1/game/gun/gun_rifle.c @@ -255,9 +255,11 @@ void Gun_Rifle_Fire(const LARA_GUN_TYPE weapon_type) for (int i = 0; i < SHOTGUN_AMMO_CLIP; i++) { dangles[0] = angles[0] - + (int)((Random_GetControl() - 16384) * PELLET_SCATTER) / 65536; + + (int32_t)((Random_GetControl() - 16384) * SHOTGUN_PELLET_SCATTER) + / 65536; dangles[1] = angles[1] - + (int)((Random_GetControl() - 16384) * PELLET_SCATTER) / 65536; + + (int32_t)((Random_GetControl() - 16384) * SHOTGUN_PELLET_SCATTER) + / 65536; if (Gun_FireWeapon(weapon_type, g_Lara.target, g_LaraItem, dangles)) { fired = true; } diff --git a/src/tr1/game/input.c b/src/tr1/game/input.c index cec011580..bb0417b1b 100644 --- a/src/tr1/game/input.c +++ b/src/tr1/game/input.c @@ -129,3 +129,9 @@ void Input_Update(void) g_InputDB.any = 0; } } + +const char *Input_GetRoleName(const INPUT_ROLE role) +{ + // TODO: implement me + return nullptr; +} diff --git a/src/tr1/game/inventory.c b/src/tr1/game/inventory.c index 79bd0df81..1674f8c3e 100644 --- a/src/tr1/game/inventory.c +++ b/src/tr1/game/inventory.c @@ -52,50 +52,50 @@ bool Inv_AddItem(const GAME_OBJECT_ID obj_id) case O_SHOTGUN_ITEM: case O_SHOTGUN_OPTION: - for (int32_t i = Inv_RequestItem(O_SG_AMMO_ITEM); i > 0; i--) { - Inv_RemoveItem(O_SG_AMMO_ITEM); - Inv_AddAmmo(&g_Lara.shotgun, SHOTGUN_AMMO_QTY); + for (int32_t i = Inv_RequestItem(O_SHOTGUN_AMMO_ITEM); i > 0; i--) { + Inv_RemoveItem(O_SHOTGUN_AMMO_ITEM); + Inv_AddAmmo(&g_Lara.shotgun_ammo, SHOTGUN_AMMO_QTY); } - Inv_AddAmmo(&g_Lara.shotgun, SHOTGUN_AMMO_QTY); + Inv_AddAmmo(&g_Lara.shotgun_ammo, SHOTGUN_AMMO_QTY); Inv_InsertItem(&g_InvRing_Item_Shotgun); - Item_GlobalReplace(O_SHOTGUN_ITEM, O_SG_AMMO_ITEM); + Item_GlobalReplace(O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM); return false; case O_MAGNUM_ITEM: case O_MAGNUM_OPTION: - for (int32_t i = Inv_RequestItem(O_MAG_AMMO_ITEM); i > 0; i--) { - Inv_RemoveItem(O_MAG_AMMO_ITEM); - Inv_AddAmmo(&g_Lara.magnums, MAGNUM_AMMO_QTY); + for (int32_t i = Inv_RequestItem(O_MAGNUM_AMMO_ITEM); i > 0; i--) { + Inv_RemoveItem(O_MAGNUM_AMMO_ITEM); + Inv_AddAmmo(&g_Lara.magnum_ammo, MAGNUM_AMMO_QTY); } - Inv_AddAmmo(&g_Lara.magnums, MAGNUM_AMMO_QTY); + Inv_AddAmmo(&g_Lara.magnum_ammo, MAGNUM_AMMO_QTY); Inv_InsertItem(&g_InvRing_Item_Magnum); - Item_GlobalReplace(O_MAGNUM_ITEM, O_MAG_AMMO_ITEM); + Item_GlobalReplace(O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM); return false; case O_UZI_ITEM: case O_UZI_OPTION: for (int32_t i = Inv_RequestItem(O_UZI_AMMO_ITEM); i > 0; i--) { Inv_RemoveItem(O_UZI_AMMO_ITEM); - Inv_AddAmmo(&g_Lara.uzis, UZI_AMMO_QTY); + Inv_AddAmmo(&g_Lara.uzi_ammo, UZI_AMMO_QTY); } - Inv_AddAmmo(&g_Lara.uzis, UZI_AMMO_QTY); + Inv_AddAmmo(&g_Lara.uzi_ammo, UZI_AMMO_QTY); Inv_InsertItem(&g_InvRing_Item_Uzi); Item_GlobalReplace(O_UZI_ITEM, O_UZI_AMMO_ITEM); return false; - case O_SG_AMMO_ITEM: - case O_SG_AMMO_OPTION: + case O_SHOTGUN_AMMO_ITEM: + case O_SHOTGUN_AMMO_OPTION: if (Inv_RequestItem(O_SHOTGUN_ITEM)) { - Inv_AddAmmo(&g_Lara.shotgun, SHOTGUN_AMMO_QTY); + Inv_AddAmmo(&g_Lara.shotgun_ammo, SHOTGUN_AMMO_QTY); } else { Inv_InsertItem(&g_InvRing_Item_ShotgunAmmo); } return false; - case O_MAG_AMMO_ITEM: - case O_MAG_AMMO_OPTION: + case O_MAGNUM_AMMO_ITEM: + case O_MAGNUM_AMMO_OPTION: if (Inv_RequestItem(O_MAGNUM_ITEM)) { - Inv_AddAmmo(&g_Lara.magnums, MAGNUM_AMMO_QTY); + Inv_AddAmmo(&g_Lara.magnum_ammo, MAGNUM_AMMO_QTY); } else { Inv_InsertItem(&g_InvRing_Item_MagnumAmmo); } @@ -104,19 +104,19 @@ bool Inv_AddItem(const GAME_OBJECT_ID obj_id) case O_UZI_AMMO_ITEM: case O_UZI_AMMO_OPTION: if (Inv_RequestItem(O_UZI_ITEM)) { - Inv_AddAmmo(&g_Lara.uzis, UZI_AMMO_QTY); + Inv_AddAmmo(&g_Lara.uzi_ammo, UZI_AMMO_QTY); } else { Inv_InsertItem(&g_InvRing_Item_UziAmmo); } return false; - case O_MEDI_ITEM: - case O_MEDI_OPTION: + case O_SMALL_MEDIPACK_ITEM: + case O_SMALL_MEDIPACK_OPTION: Inv_InsertItem(&g_InvRing_Item_Medi); return true; - case O_BIGMEDI_ITEM: - case O_BIGMEDI_OPTION: + case O_LARGE_MEDIPACK_ITEM: + case O_LARGE_MEDIPACK_OPTION: Inv_InsertItem(&g_InvRing_Item_BigMedi); return true; diff --git a/src/tr1/game/inventory_ring/control.c b/src/tr1/game/inventory_ring/control.c index b48227895..760b66a9b 100644 --- a/src/tr1/game/inventory_ring/control.c +++ b/src/tr1/game/inventory_ring/control.c @@ -23,6 +23,7 @@ #include "global/vars.h" #include +#include #include #include #include @@ -96,7 +97,7 @@ static void M_RemoveExamineOverlay(void) static void M_ShowAmmoQuantity(const char *const fmt, const int32_t qty) { - if (!(g_GameInfo.bonus_flag & GBF_NGPLUS)) { + if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { InvRing_ShowItemQuantity(fmt, qty); } } @@ -122,28 +123,29 @@ static void M_RingNotActive(const INVENTORY_ITEM *const inv_item) switch (inv_item->object_id) { case O_SHOTGUN_OPTION: - M_ShowAmmoQuantity("%5d A", g_Lara.shotgun.ammo / SHOTGUN_AMMO_CLIP); + M_ShowAmmoQuantity( + "%5d A", g_Lara.shotgun_ammo.ammo / SHOTGUN_AMMO_CLIP); break; case O_MAGNUM_OPTION: - M_ShowAmmoQuantity("%5d B", g_Lara.magnums.ammo); + M_ShowAmmoQuantity("%5d B", g_Lara.magnum_ammo.ammo); break; case O_UZI_OPTION: - M_ShowAmmoQuantity("%5d C", g_Lara.uzis.ammo); + M_ShowAmmoQuantity("%5d C", g_Lara.uzi_ammo.ammo); break; - case O_SG_AMMO_OPTION: - InvRing_ShowItemQuantity("%d", qty * NUM_SG_SHELLS); + case O_SHOTGUN_AMMO_OPTION: + InvRing_ShowItemQuantity("%d", qty * SHOTGUN_SHELL_COUNT); break; - case O_MAG_AMMO_OPTION: + case O_MAGNUM_AMMO_OPTION: case O_UZI_AMMO_OPTION: InvRing_ShowItemQuantity("%d", qty * 2); break; - case O_MEDI_OPTION: - case O_BIGMEDI_OPTION: + case O_SMALL_MEDIPACK_OPTION: + case O_LARGE_MEDIPACK_OPTION: Overlay_BarSetHealthTimer(40); if (qty > 1) { InvRing_ShowItemQuantity("%d", qty); @@ -174,8 +176,8 @@ static void M_RingNotActive(const INVENTORY_ITEM *const inv_item) break; } - if (inv_item->object_id == O_MEDI_OPTION - || inv_item->object_id == O_BIGMEDI_OPTION) { + if (inv_item->object_id == O_SMALL_MEDIPACK_OPTION + || inv_item->object_id == O_LARGE_MEDIPACK_OPTION) { if (g_Config.ui.healthbar_location == BL_TOP_LEFT) { InvRing_HideArrow(INV_RING_ARROW_TL, true); } else if (g_Config.ui.healthbar_location == BL_TOP_RIGHT) { @@ -315,8 +317,8 @@ static GF_COMMAND M_Finish(INV_RING *const ring, const bool apply_changes) case O_SHOTGUN_OPTION: case O_MAGNUM_OPTION: case O_UZI_OPTION: - case O_MEDI_OPTION: - case O_BIGMEDI_OPTION: + case O_SMALL_MEDIPACK_OPTION: + case O_LARGE_MEDIPACK_OPTION: case O_KEY_OPTION_1: case O_KEY_OPTION_2: case O_KEY_OPTION_3: @@ -864,7 +866,6 @@ INV_RING *InvRing_Open(const INVENTORY_MODE mode) g_InvRing_Source[RT_OPTION].count = TITLE_RING_OBJECTS; InvRing_ShowVersionText(); Savegame_ScanSavedGames(); - Savegame_FillAvailableSaves(&g_SavegameRequester); } else { g_InvRing_Source[RT_OPTION].count = OPTION_RING_OBJECTS; InvRing_RemoveVersionText(); @@ -929,7 +930,7 @@ INV_RING *InvRing_Open(const INVENTORY_MODE mode) break; } - g_InvMode = mode; + g_Inv_Mode = mode; Interpolation_Remember(); if (g_Config.gameplay.enable_timer_in_inventory) { Stats_StartTimer(); diff --git a/src/tr1/game/inventory_ring/draw.c b/src/tr1/game/inventory_ring/draw.c index f7ab338f3..5c2ab63c3 100644 --- a/src/tr1/game/inventory_ring/draw.c +++ b/src/tr1/game/inventory_ring/draw.c @@ -70,9 +70,9 @@ static void M_DrawItem( { if (ring->motion.status != RNG_FADING_OUT && ring->motion.status != RNG_DONE && inv_item == ring->list[ring->current_object] && !ring->rotating) { - Output_SetLightAdder(HIGH_LIGHT); + Output_SetLightAdder(SHADE_NEUTRAL); } else { - Output_SetLightAdder(LOW_LIGHT); + Output_SetLightAdder(SHADE_LOW); } Matrix_TranslateRel(0, inv_item->y_trans, inv_item->z_trans); @@ -81,7 +81,6 @@ static void M_DrawItem( OBJECT *const obj = Object_Get(inv_item->object_id); if (obj->mesh_count < 0) { - Output_DrawSpriteRel(0, 0, 0, obj->mesh_idx, 4096); return; } @@ -91,7 +90,7 @@ static void M_DrawItem( const int32_t frac = M_GetFrames(ring, inv_item, &frame1, &frame2, &rate); if (inv_item->object_id == O_COMPASS_OPTION) { const int16_t extra_rotation[1] = { Option_Compass_GetNeedleAngle() }; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; Object_DrawInterpolatedObject( obj, inv_item->meshes_drawn, extra_rotation, frame1, frame2, frac, rate); @@ -169,8 +168,8 @@ void InvRing_Draw(INV_RING *const ring) || ring->motion.status == RNG_CLOSING_ITEM)) { const INVENTORY_ITEM *inv_item = ring->list[ring->current_object]; switch (inv_item->object_id) { - case O_MEDI_OPTION: - case O_BIGMEDI_OPTION: + case O_SMALL_MEDIPACK_OPTION: + case O_LARGE_MEDIPACK_OPTION: if (g_Config.ui.enable_game_ui) { Overlay_BarDrawHealth(); } diff --git a/src/tr1/game/inventory_ring/vars.c b/src/tr1/game/inventory_ring/vars.c index eba55d339..f0850ec5e 100644 --- a/src/tr1/game/inventory_ring/vars.c +++ b/src/tr1/game/inventory_ring/vars.c @@ -80,7 +80,7 @@ INVENTORY_ITEM g_InvRing_Item_Compass = { }; INVENTORY_ITEM g_InvRing_Item_Medi = { - .object_id = O_MEDI_OPTION, + .object_id = O_SMALL_MEDIPACK_OPTION, .frames_total = 26, .current_frame = 0, .goal_frame = 0, @@ -105,7 +105,7 @@ INVENTORY_ITEM g_InvRing_Item_Medi = { }; INVENTORY_ITEM g_InvRing_Item_BigMedi = { - .object_id = O_BIGMEDI_OPTION, + .object_id = O_LARGE_MEDIPACK_OPTION, .frames_total = 20, .current_frame = 0, .goal_frame = 0, @@ -580,7 +580,7 @@ INVENTORY_ITEM g_InvRing_Item_PistolAmmo = { }; INVENTORY_ITEM g_InvRing_Item_ShotgunAmmo = { - .object_id = O_SG_AMMO_OPTION, + .object_id = O_SHOTGUN_AMMO_OPTION, .frames_total = 1, .current_frame = 0, .goal_frame = 0, @@ -605,7 +605,7 @@ INVENTORY_ITEM g_InvRing_Item_ShotgunAmmo = { }; INVENTORY_ITEM g_InvRing_Item_MagnumAmmo = { - .object_id = O_MAG_AMMO_OPTION, + .object_id = O_MAGNUM_AMMO_OPTION, .frames_total = 1, .current_frame = 0, .goal_frame = 0, diff --git a/src/tr1/game/items.c b/src/tr1/game/items.c index c2473382a..bcc068aa1 100644 --- a/src/tr1/game/items.c +++ b/src/tr1/game/items.c @@ -11,6 +11,7 @@ #include "global/vars.h" #include +#include #include #include #include @@ -27,6 +28,7 @@ } \ } while (0) +static BOUNDS_16 m_NullBounds = {}; static BOUNDS_16 m_InterpolatedBounds = {}; void Item_Control(void) @@ -92,7 +94,7 @@ void Item_Initialise(int16_t item_num) Room_GetWorldSector(room, item->pos.x, item->pos.z); item->floor = sector->floor.height; - if (g_GameInfo.bonus_flag & GBF_NGPLUS) { + if (Game_IsBonusFlagSet(GBF_NGPLUS)) { item->hit_points *= 2; } if (obj->initialise_func) { @@ -148,7 +150,7 @@ int16_t Item_Spawn(const ITEM *const item, const GAME_OBJECT_ID obj_id) spawn->rot = item->rot; Item_Initialise(spawn_num); spawn->status = IS_INACTIVE; - spawn->shade.value_1 = HIGH_LIGHT; + spawn->shade.value_1 = SHADE_NEUTRAL; } return spawn_num; } @@ -194,30 +196,6 @@ bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range) && (SQUARE(x) + SQUARE(y) + SQUARE(z) < SQUARE(range)); } -bool Item_TestBoundsCollide(ITEM *src_item, ITEM *dst_item, int32_t radius) -{ - const BOUNDS_16 *const src_bounds = &Item_GetBestFrame(src_item)->bounds; - const BOUNDS_16 *const dst_bounds = &Item_GetBestFrame(dst_item)->bounds; - if (dst_item->pos.y + dst_bounds->max.y - <= src_item->pos.y + src_bounds->min.y - || dst_item->pos.y + dst_bounds->min.y - >= src_item->pos.y + src_bounds->max.y) { - return false; - } - - const int32_t c = Math_Cos(dst_item->rot.y); - const int32_t s = Math_Sin(dst_item->rot.y); - const int32_t x = src_item->pos.x - dst_item->pos.x; - const int32_t z = src_item->pos.z - dst_item->pos.z; - const int32_t rx = (c * x - s * z) >> W2V_SHIFT; - const int32_t rz = (c * z + s * x) >> W2V_SHIFT; - const int32_t min_x = dst_bounds->min.x - radius; - const int32_t max_x = dst_bounds->max.x + radius; - const int32_t min_z = dst_bounds->min.z - radius; - const int32_t max_z = dst_bounds->max.z + radius; - return rx >= min_x && rx <= max_x && rz >= min_z && rz <= max_z; -} - bool Item_TestPosition( const ITEM *const src_item, const ITEM *const dst_item, const OBJECT_BOUNDS *const bounds) @@ -383,55 +361,29 @@ void Item_ShiftCol(ITEM *item, COLL_INFO *coll) coll->shift.z = 0; } -bool Item_IsTriggerActive(ITEM *item) -{ - bool ok = !(item->flags & IF_REVERSE); - - if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) { - return !ok; - } - - if (!item->timer) { - return ok; - } - - if (item->timer == -1) { - return !ok; - } - - item->timer--; - - if (!item->timer) { - item->timer = -1; - } - - return ok; -} - ANIM_FRAME *Item_GetBestFrame(const ITEM *item) { - ANIM_FRAME *frmptr[2]; + ANIM_FRAME *frames[2]; int32_t rate; - int32_t frac = Item_GetFrames(item, frmptr, &rate); - if (frac <= rate / 2) { - return frmptr[0]; - } else { - return frmptr[1]; - } + const int32_t frac = Item_GetFrames(item, frames, &rate); + return frames[(frac > rate / 2) ? 1 : 0]; } const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item) { int32_t rate; - ANIM_FRAME *frmptr[2]; - - int32_t frac = Item_GetFrames(item, frmptr, &rate); - if (!frac) { - return &frmptr[0]->bounds; + ANIM_FRAME *frames[2]; + const int32_t frac = Item_GetFrames(item, frames, &rate); + if (frames[0] == nullptr) { + return &m_NullBounds; } - const BOUNDS_16 *const a = &frmptr[0]->bounds; - const BOUNDS_16 *const b = &frmptr[1]->bounds; + if (frac == 0) { + return &frames[0]->bounds; + } + + const BOUNDS_16 *const a = &frames[0]->bounds; + const BOUNDS_16 *const b = &frames[1]->bounds; BOUNDS_16 *const result = &m_InterpolatedBounds; result->min.x = a->min.x + (((b->min.x - a->min.x) * frac) / rate); @@ -443,9 +395,13 @@ const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item) return result; } -int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate) +int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frames[], int32_t *rate) { const ANIM *const anim = Item_GetAnim(item); + if (anim->frame_ptr == nullptr) { + frames[0] = nullptr; + return 0; + } const int32_t cur_frame_num = item->frame_num - anim->frame_base; const int32_t last_frame_num = anim->frame_end - anim->frame_base; @@ -453,8 +409,8 @@ int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate) const int32_t first_key_frame_num = cur_frame_num / key_frame_span; const int32_t second_key_frame_num = first_key_frame_num + 1; - frmptr[0] = &anim->frame_ptr[first_key_frame_num]; - frmptr[1] = &anim->frame_ptr[second_key_frame_num]; + frames[0] = &anim->frame_ptr[first_key_frame_num]; + frames[1] = &anim->frame_ptr[second_key_frame_num]; const int32_t key_frame_shift = cur_frame_num % key_frame_span; const int32_t numerator = key_frame_shift; @@ -503,12 +459,6 @@ int32_t Item_Explode(int16_t item_num, int32_t mesh_bits, int16_t damage) Matrix_TranslateRel16(frame->offset); Matrix_Rot16(frame->mesh_rots[0]); -#if 0 - // XXX: present in OG, removed by GLrage on the grounds that it sometimes - // crashes. - int16_t *extra_rotation = (int16_t*)item->data; -#endif - int32_t bit = 1; if ((bit & mesh_bits) && (bit & item->mesh_bits)) { int16_t effect_num = Effect_Create(item->room_num); @@ -546,17 +496,10 @@ int32_t Item_Explode(int16_t item_num, int32_t mesh_bits, int16_t damage) Matrix_Rot16(frame->mesh_rots[i]); #if 0 - if (extra_rotation) { - if (bone->rot_y) { - Matrix_RotY(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ(*extra_rotation++); - } - } + // XXX: present in OG, removed by GLrage on the grounds that it + // sometimes crashes. + const int16_t *extra_rotation = (int16_t *)item->data; + Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); #endif bit <<= 1; diff --git a/src/tr1/game/items.h b/src/tr1/game/items.h index b143055ad..a9bc07b68 100644 --- a/src/tr1/game/items.h +++ b/src/tr1/game/items.h @@ -11,7 +11,6 @@ int16_t Item_Spawn(const ITEM *item, GAME_OBJECT_ID obj_id); bool Item_IsNearItem(const ITEM *item, const XYZ_32 *pos, int32_t distance); bool Item_Test3DRange(int32_t x, int32_t y, int32_t z, int32_t range); -bool Item_TestBoundsCollide(ITEM *src_item, ITEM *dst_item, int32_t radius); bool Item_TestPosition( const ITEM *src_item, const ITEM *dst_item, const OBJECT_BOUNDS *bounds); void Item_AlignPosition(ITEM *src_item, ITEM *dst_item, XYZ_32 *vec); @@ -20,9 +19,6 @@ bool Item_MovePosition( void Item_ShiftCol(ITEM *item, COLL_INFO *coll); int32_t Item_GetDistance(const ITEM *item, const XYZ_32 *target); -bool Item_IsTriggerActive(ITEM *item); - -const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item); int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate); void Item_TakeDamage(ITEM *item, int16_t damage, bool hit_status); diff --git a/src/tr1/game/lara/cheat.c b/src/tr1/game/lara/cheat.c index b656e36e0..cca271687 100644 --- a/src/tr1/game/lara/cheat.c +++ b/src/tr1/game/lara/cheat.c @@ -107,9 +107,9 @@ void Lara_Cheat_Control(void) Inv_AddItem(O_SHOTGUN_ITEM); Inv_AddItem(O_MAGNUM_ITEM); Inv_AddItem(O_UZI_ITEM); - g_Lara.shotgun.ammo = 500; - g_Lara.magnums.ammo = 500; - g_Lara.uzis.ammo = 5000; + g_Lara.shotgun_ammo.ammo = 500; + g_Lara.magnum_ammo.ammo = 500; + g_Lara.uzi_ammo.ammo = 5000; Sound_Effect(SFX_LARA_HOLSTER, nullptr, SPM_ALWAYS); } else if (as == LS_SWAN_DIVE) { Item_Explode(g_Lara.item_num, -1, 1); @@ -241,9 +241,9 @@ bool Lara_Cheat_GiveAllGuns(void) Inv_AddItem(O_MAGNUM_ITEM); Inv_AddItem(O_UZI_ITEM); Inv_AddItem(O_SHOTGUN_ITEM); - g_Lara.shotgun.ammo = g_GameInfo.bonus_flag & GBF_NGPLUS ? 10001 : 300; - g_Lara.magnums.ammo = g_GameInfo.bonus_flag & GBF_NGPLUS ? 10001 : 1000; - g_Lara.uzis.ammo = g_GameInfo.bonus_flag & GBF_NGPLUS ? 10001 : 2000; + g_Lara.shotgun_ammo.ammo = Game_IsBonusFlagSet(GBF_NGPLUS) ? 10001 : 300; + g_Lara.magnum_ammo.ammo = Game_IsBonusFlagSet(GBF_NGPLUS) ? 10001 : 1000; + g_Lara.uzi_ammo.ammo = Game_IsBonusFlagSet(GBF_NGPLUS) ? 10001 : 2000; Sound_Effect(SFX_LARA_RELOAD, nullptr, SPM_ALWAYS); Console_Log(GS(OSD_GIVE_ITEM_ALL_GUNS)); @@ -261,24 +261,24 @@ bool Lara_Cheat_GiveAllItems(void) if (!Inv_RequestItem(O_SHOTGUN_ITEM)) { Inv_AddItem(O_SHOTGUN_ITEM); } - g_Lara.shotgun.ammo = g_GameInfo.bonus_flag & GBF_NGPLUS ? 10001 : 300; + g_Lara.shotgun_ammo.ammo = Game_IsBonusFlagSet(GBF_NGPLUS) ? 10001 : 300; if (!Inv_RequestItem(O_MAGNUM_ITEM)) { Inv_AddItem(O_MAGNUM_ITEM); } - g_Lara.magnums.ammo = g_GameInfo.bonus_flag & GBF_NGPLUS ? 10001 : 1000; + g_Lara.magnum_ammo.ammo = Game_IsBonusFlagSet(GBF_NGPLUS) ? 10001 : 1000; if (!Inv_RequestItem(O_UZI_ITEM)) { Inv_AddItem(O_UZI_ITEM); } - g_Lara.uzis.ammo = g_GameInfo.bonus_flag & GBF_NGPLUS ? 10001 : 2000; + g_Lara.uzi_ammo.ammo = Game_IsBonusFlagSet(GBF_NGPLUS) ? 10001 : 2000; for (int i = 0; i < 10; i++) { - if (Inv_RequestItem(O_MEDI_ITEM) < 240) { - Inv_AddItem(O_MEDI_ITEM); + if (Inv_RequestItem(O_SMALL_MEDIPACK_ITEM) < 240) { + Inv_AddItem(O_SMALL_MEDIPACK_ITEM); } - if (Inv_RequestItem(O_BIGMEDI_ITEM) < 240) { - Inv_AddItem(O_BIGMEDI_ITEM); + if (Inv_RequestItem(O_LARGE_MEDIPACK_ITEM) < 240) { + Inv_AddItem(O_LARGE_MEDIPACK_ITEM); } } @@ -383,13 +383,23 @@ bool Lara_Cheat_KillEnemy(const int16_t item_num) return true; } -bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z) +bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z, int16_t room_num) { - int16_t room_num = Room_GetIndexFromPos(x, y, z); + if (room_num == NO_ROOM) { + room_num = Room_GetIndexFromPos(x, y, z); + } if (room_num == NO_ROOM) { return false; } + const ROOM *const room = Room_Get(room_num); + if (room->flip_status == RFS_FLIPPED && Room_GetFlipStatus()) { + room_num = Room_GetFlippedBaseRoom(room_num); + if (room_num == NO_ROOM) { + return false; + } + } + const SECTOR *sector = Room_GetSector(x, y, z, &room_num); int16_t height = Room_GetHeight(sector, x, y, z); @@ -411,12 +421,9 @@ bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z) .y = y, .z = ROUND_TO_SECTOR(z + dz * unit) + WALL_L / 2, }; - room_num = Room_GetIndexFromPos(point.x, point.y, point.z); - if (room_num == NO_ROOM) { - continue; - } sector = Room_GetSector(point.x, point.y, point.z, &room_num); - height = Room_GetHeight(sector, point.x, point.y, point.z); + height = + Room_GetHeightEx(sector, point.x, point.y, point.z, true); if (height == NO_HEIGHT) { continue; } @@ -443,12 +450,8 @@ bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z) } } - room_num = Room_GetIndexFromPos(x, y, z); - if (room_num == NO_ROOM) { - return false; - } sector = Room_GetSector(x, y, z, &room_num); - height = Room_GetHeight(sector, x, y, z); + height = Room_GetHeightEx(sector, x, y, z, true); if (height == NO_HEIGHT) { return false; } diff --git a/src/tr1/game/lara/cheat.h b/src/tr1/game/lara/cheat.h index ab2e5050f..6214b5832 100644 --- a/src/tr1/game/lara/cheat.h +++ b/src/tr1/game/lara/cheat.h @@ -11,4 +11,3 @@ bool Lara_Cheat_GiveAllGuns(void); bool Lara_Cheat_GiveAllItems(void); bool Lara_Cheat_OpenNearestDoor(void); bool Lara_Cheat_KillEnemy(int16_t item_num); -bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z); diff --git a/src/tr1/game/lara/common.c b/src/tr1/game/lara/common.c index 863527a7c..4e1c95406 100644 --- a/src/tr1/game/lara/common.c +++ b/src/tr1/game/lara/common.c @@ -273,11 +273,7 @@ void Lara_Control(void) item->hit_points = -1; if (!g_Lara.death_timer) { Music_Stop(); - g_GameInfo.death_count++; - if (Savegame_GetBoundSlot() != -1) { - Savegame_UpdateDeathCounters( - Savegame_GetBoundSlot(), &g_GameInfo); - } + Stats_AddDeath(); } g_Lara.death_timer++; // make sure the enemy healthbar is no longer rendered. If g_Lara later @@ -396,28 +392,28 @@ void Lara_UseItem(const GAME_OBJECT_ID obj_id) } break; - case O_MEDI_ITEM: - case O_MEDI_OPTION: + case O_SMALL_MEDIPACK_ITEM: + case O_SMALL_MEDIPACK_OPTION: if (g_LaraItem->hit_points <= 0 || g_LaraItem->hit_points >= LARA_MAX_HITPOINTS) { return; } g_LaraItem->hit_points += LARA_MAX_HITPOINTS / 2; CLAMPG(g_LaraItem->hit_points, LARA_MAX_HITPOINTS); - Inv_RemoveItem(O_MEDI_ITEM); + Inv_RemoveItem(O_SMALL_MEDIPACK_ITEM); Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS); Stats_AddMedipacksUsed(.5); break; - case O_BIGMEDI_ITEM: - case O_BIGMEDI_OPTION: + case O_LARGE_MEDIPACK_ITEM: + case O_LARGE_MEDIPACK_OPTION: if (g_LaraItem->hit_points <= 0 || g_LaraItem->hit_points >= LARA_MAX_HITPOINTS) { return; } g_LaraItem->hit_points = g_LaraItem->hit_points + LARA_MAX_HITPOINTS; CLAMPG(g_LaraItem->hit_points, LARA_MAX_HITPOINTS); - Inv_RemoveItem(O_BIGMEDI_ITEM); + Inv_RemoveItem(O_LARGE_MEDIPACK_ITEM); Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS); Stats_AddMedipacksUsed(1); break; @@ -545,14 +541,14 @@ void Lara_InitialiseInventory(const GF_LEVEL *const level) RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); - g_Lara.pistols.ammo = 1000; + g_Lara.pistol_ammo.ammo = 1000; if (resume != nullptr) { if (g_GameInfo.remove_guns) { - resume->flags.got_pistols = 0; - resume->flags.got_shotgun = 0; - resume->flags.got_magnums = 0; - resume->flags.got_uzis = 0; + resume->flags.has_pistols = 0; + resume->flags.has_shotgun = 0; + resume->flags.has_magnums = 0; + resume->flags.has_uzis = 0; resume->equipped_gun_type = LGT_UNARMED; resume->holsters_gun_type = LGT_UNARMED; resume->back_gun_type = LGT_UNARMED; @@ -570,60 +566,60 @@ void Lara_InitialiseInventory(const GF_LEVEL *const level) } if (g_GameInfo.remove_medipacks) { - resume->num_medis = 0; - resume->num_big_medis = 0; + resume->small_medipacks = 0; + resume->large_medipacks = 0; } - if (resume->flags.got_pistols) { + if (resume->flags.has_pistols) { Inv_AddItem(O_PISTOL_ITEM); } - if (resume->flags.got_magnums) { + if (resume->flags.has_magnums) { Inv_AddItem(O_MAGNUM_ITEM); - g_Lara.magnums.ammo = resume->magnum_ammo; - Item_GlobalReplace(O_MAGNUM_ITEM, O_MAG_AMMO_ITEM); + g_Lara.magnum_ammo.ammo = resume->magnum_ammo; + Item_GlobalReplace(O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM); } else { int32_t ammo = resume->magnum_ammo / MAGNUM_AMMO_QTY; for (int i = 0; i < ammo; i++) { - Inv_AddItem(O_MAG_AMMO_ITEM); + Inv_AddItem(O_MAGNUM_AMMO_ITEM); } - g_Lara.magnums.ammo = 0; + g_Lara.magnum_ammo.ammo = 0; } - if (resume->flags.got_uzis) { + if (resume->flags.has_uzis) { Inv_AddItem(O_UZI_ITEM); - g_Lara.uzis.ammo = resume->uzi_ammo; + g_Lara.uzi_ammo.ammo = resume->uzi_ammo; Item_GlobalReplace(O_UZI_ITEM, O_UZI_AMMO_ITEM); } else { int32_t ammo = resume->uzi_ammo / UZI_AMMO_QTY; for (int i = 0; i < ammo; i++) { Inv_AddItem(O_UZI_AMMO_ITEM); } - g_Lara.uzis.ammo = 0; + g_Lara.uzi_ammo.ammo = 0; } - if (resume->flags.got_shotgun) { + if (resume->flags.has_shotgun) { Inv_AddItem(O_SHOTGUN_ITEM); - g_Lara.shotgun.ammo = resume->shotgun_ammo; - Item_GlobalReplace(O_SHOTGUN_ITEM, O_SG_AMMO_ITEM); + g_Lara.shotgun_ammo.ammo = resume->shotgun_ammo; + Item_GlobalReplace(O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM); } else { int32_t ammo = resume->shotgun_ammo / SHOTGUN_AMMO_QTY; for (int i = 0; i < ammo; i++) { - Inv_AddItem(O_SG_AMMO_ITEM); + Inv_AddItem(O_SHOTGUN_AMMO_ITEM); } - g_Lara.shotgun.ammo = 0; + g_Lara.shotgun_ammo.ammo = 0; } for (int i = 0; i < resume->num_scions; i++) { Inv_AddItem(O_SCION_ITEM_1); } - for (int i = 0; i < resume->num_medis; i++) { - Inv_AddItem(O_MEDI_ITEM); + for (int i = 0; i < resume->small_medipacks; i++) { + Inv_AddItem(O_SMALL_MEDIPACK_ITEM); } - for (int i = 0; i < resume->num_big_medis; i++) { - Inv_AddItem(O_BIGMEDI_ITEM); + for (int i = 0; i < resume->large_medipacks; i++) { + Inv_AddItem(O_LARGE_MEDIPACK_ITEM); } g_Lara.gun_status = resume->gun_status; @@ -705,11 +701,6 @@ bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance) return Item_IsNearItem(g_LaraItem, pos, distance); } -bool Lara_TestBoundsCollide(ITEM *item, int32_t radius) -{ - return Item_TestBoundsCollide(g_LaraItem, item, radius); -} - bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *const bounds) { return Item_TestPosition(g_LaraItem, item, bounds); @@ -730,11 +721,13 @@ bool Lara_MovePosition(ITEM *item, XYZ_32 *vec) return Item_MovePosition(g_LaraItem, item, vec, velocity); } -void Lara_Push(ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push) +void Lara_Push( + const ITEM *const item, COLL_INFO *const coll, const bool hit_on, + const bool big_push) { - ITEM *const lara_item = g_LaraItem; - int32_t x = lara_item->pos.x - item->pos.x; - int32_t z = lara_item->pos.z - item->pos.z; + ITEM *const target_item = Lara_GetItem(); + int32_t x = target_item->pos.x - item->pos.x; + int32_t z = target_item->pos.z - item->pos.z; const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); int32_t rx = (c * x - s * z) >> W2V_SHIFT; @@ -772,8 +765,8 @@ void Lara_Push(ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push) int32_t ax = (c * rx + s * rz) >> W2V_SHIFT; int32_t az = (c * rz - s * rx) >> W2V_SHIFT; - lara_item->pos.x = item->pos.x + ax; - lara_item->pos.z = item->pos.z + az; + target_item->pos.x = item->pos.x + ax; + target_item->pos.z = item->pos.z + az; rx = (bounds->min.x + bounds->max.x) / 2; rz = (bounds->min.z + bounds->max.z) / 2; @@ -781,10 +774,10 @@ void Lara_Push(ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push) z -= (c * rz - s * rx) >> W2V_SHIFT; if (hit_on) { - PHD_ANGLE hitang = lara_item->rot.y - (DEG_180 + Math_Atan(z, x)); + PHD_ANGLE hitang = target_item->rot.y - (DEG_180 + Math_Atan(z, x)); g_Lara.hit_direction = (hitang + DEG_45) / DEG_90; if (!g_Lara.hit_frame) { - Sound_Effect(SFX_LARA_BODYSL, &lara_item->pos, SPM_NORMAL); + Sound_Effect(SFX_LARA_BODYSL, &target_item->pos, SPM_NORMAL); } g_Lara.hit_frame++; @@ -799,20 +792,20 @@ void Lara_Push(ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push) int16_t old_facing = coll->facing; coll->facing = Math_Atan( - lara_item->pos.z - coll->old.z, lara_item->pos.x - coll->old.x); + target_item->pos.z - coll->old.z, target_item->pos.x - coll->old.x); Collide_GetCollisionInfo( - coll, lara_item->pos.x, lara_item->pos.y, lara_item->pos.z, - lara_item->room_num, LARA_HEIGHT); + coll, target_item->pos.x, target_item->pos.y, target_item->pos.z, + target_item->room_num, LARA_HEIGHT); coll->facing = old_facing; if (coll->coll_type != COLL_NONE) { - lara_item->pos.x = coll->old.x; - lara_item->pos.z = coll->old.z; + target_item->pos.x = coll->old.x; + target_item->pos.z = coll->old.z; } else { - coll->old.x = lara_item->pos.x; - coll->old.y = lara_item->pos.y; - coll->old.z = lara_item->pos.z; - Item_UpdateRoom(item, -10); + coll->old.x = target_item->pos.x; + coll->old.y = target_item->pos.y; + coll->old.z = target_item->pos.z; + Item_UpdateRoom(target_item, -10); } if (g_Lara.interact_target.is_moving diff --git a/src/tr1/game/lara/common.h b/src/tr1/game/lara/common.h index 854fae41e..291ab20e9 100644 --- a/src/tr1/game/lara/common.h +++ b/src/tr1/game/lara/common.h @@ -26,10 +26,8 @@ void Lara_SwapMeshExtra(void); bool Lara_IsNearItem(const XYZ_32 *pos, int32_t distance); void Lara_UseItem(GAME_OBJECT_ID obj_id); -bool Lara_TestBoundsCollide(ITEM *item, int32_t radius); bool Lara_TestPosition(const ITEM *item, const OBJECT_BOUNDS *bounds); void Lara_AlignPosition(ITEM *item, XYZ_32 *vec); bool Lara_MovePosition(ITEM *item, XYZ_32 *vec); -void Lara_Push(ITEM *item, COLL_INFO *coll, bool hit_on, bool big_push); void Lara_RevertToPistolsIfNeeded(void); diff --git a/src/tr1/game/lara/misc.h b/src/tr1/game/lara/misc.h index 9e918e723..6b71bfa7e 100644 --- a/src/tr1/game/lara/misc.h +++ b/src/tr1/game/lara/misc.h @@ -23,4 +23,3 @@ int32_t Lara_GetWaterDepth(int32_t x, int32_t y, int32_t z, int16_t room_num); void Lara_TestWaterDepth(ITEM *item, const COLL_INFO *coll); bool Lara_TestWaterStepOut(ITEM *item, const COLL_INFO *coll); bool Lara_TestWaterClimbOut(ITEM *item, COLL_INFO *coll); -void Lara_CatchFire(void); diff --git a/src/tr1/game/level.c b/src/tr1/game/level.c index 54e4a1db0..4000c3edc 100644 --- a/src/tr1/game/level.c +++ b/src/tr1/game/level.c @@ -4,7 +4,6 @@ #include "game/carrier.h" #include "game/effects.h" #include "game/game.h" -#include "game/game_flow.h" #include "game/inventory_ring/vars.h" #include "game/items.h" #include "game/lara/common.h" @@ -54,7 +53,6 @@ static LEVEL_LAYOUT M_GuessLayout(VFILE *file); static void M_LoadFromFile(const GF_LEVEL *level); static void M_CompleteSetup(const GF_LEVEL *level); static void M_MarkWaterEdgeVertices(void); -static size_t M_CalculateMaxVertices(void); static bool M_TryLayout(VFILE *const file, const LEVEL_LAYOUT layout) { @@ -261,15 +259,11 @@ static void M_CompleteSetup(const GF_LEVEL *const level) // Configure enemies who carry and drop items Carrier_InitialiseLevel(level); - const size_t max_vertices = M_CalculateMaxVertices(); - LOG_INFO("Maximum vertices: %d", max_vertices); - Output_ReserveVertexBuffer(max_vertices); - Level_LoadTextures(); Level_LoadTexturePages(); Level_LoadPalettes(); Level_LoadFaces(); - Output_DownloadTextures(); + Output_ObserveLevelLoad(); // Initialise the sound effects. LEVEL_INFO *const info = Level_GetInfo(); @@ -320,41 +314,6 @@ static void M_MarkWaterEdgeVertices(void) Benchmark_End(&benchmark, nullptr); } -static size_t M_CalculateMaxVertices(void) -{ - BENCHMARK benchmark = Benchmark_Start(); - int32_t max_vertices = 0; - for (int32_t i = 0; i < O_NUMBER_OF; i++) { - const OBJECT *const obj = Object_Get(i); - if (!obj->loaded) { - continue; - } - - for (int32_t j = 0; j < obj->mesh_count; j++) { - const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + j); - max_vertices = MAX(max_vertices, mesh->num_vertices); - } - } - - for (int32_t i = 0; i < MAX_STATIC_OBJECTS; i++) { - const STATIC_OBJECT_3D *obj = Object_Get3DStatic(i); - if (!obj->loaded) { - continue; - } - - const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx); - max_vertices = MAX(max_vertices, mesh->num_vertices); - } - - for (int32_t i = 0; i < Room_GetCount(); i++) { - const ROOM *const room = Room_Get(i); - max_vertices = MAX(max_vertices, room->mesh.num_vertices); - } - - Benchmark_End(&benchmark, nullptr); - return max_vertices; -} - void Level_Load(const GF_LEVEL *const level) { LOG_INFO("%d (%s)", level->num, level->path); @@ -367,15 +326,17 @@ void Level_Load(const GF_LEVEL *const level) Inject_Cleanup(); - Output_SetWaterColor(&level->settings.water_color); - Output_SetDrawDistFade(level->settings.draw_distance_fade * WALL_L); - Output_SetDrawDistMax(level->settings.draw_distance_max * WALL_L); Output_SetSkyboxEnabled( g_Config.visuals.enable_skybox && Object_Get(O_SKYBOX)->loaded); Benchmark_End(&benchmark, nullptr); } +void Level_Unload(void) +{ + Output_ObserveLevelUnload(); +} + bool Level_Initialise( const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx) { @@ -411,19 +372,12 @@ bool Level_Initialise( Music_ResetTrackFlags(); - /* Clear Object Loaded flags */ - for (int32_t i = 0; i < O_NUMBER_OF; i++) { - Object_Get(i)->loaded = false; - } - for (int32_t i = 0; i < MAX_STATIC_OBJECTS; i++) { - Object_Get2DStatic(i)->loaded = false; - Object_Get3DStatic(i)->loaded = false; - } - + Object_Reset(); Camera_Reset(); Pierre_Reset(); Lara_InitialiseLoad(NO_ITEM); + Level_Unload(); Level_Load(level); GameStringTable_Apply(level); diff --git a/src/tr1/game/level.h b/src/tr1/game/level.h index 986c528f1..00a5b0856 100644 --- a/src/tr1/game/level.h +++ b/src/tr1/game/level.h @@ -2,5 +2,7 @@ #include "game/game_flow/types.h" +#include + bool Level_Initialise(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); void Level_Load(const GF_LEVEL *level); diff --git a/src/tr1/game/los.h b/src/tr1/game/los.h index 0a5981260..96041ee31 100644 --- a/src/tr1/game/los.h +++ b/src/tr1/game/los.h @@ -1,5 +1,3 @@ #pragma once -#include "global/types.h" - -bool LOS_Check(const GAME_VECTOR *start, GAME_VECTOR *target); +#include diff --git a/src/tr1/game/lot.c b/src/tr1/game/lot.c index 11f9a6f52..16691919a 100644 --- a/src/tr1/game/lot.c +++ b/src/tr1/game/lot.c @@ -16,8 +16,8 @@ static CREATURE *m_BaddieSlots = nullptr; void LOT_InitialiseArray(void) { m_BaddieSlots = - GameBuf_Alloc(NUM_SLOTS * sizeof(CREATURE), GBUF_CREATURE_DATA); - for (int i = 0; i < NUM_SLOTS; i++) { + GameBuf_Alloc(LOT_SLOT_COUNT * sizeof(CREATURE), GBUF_CREATURE_DATA); + for (int i = 0; i < LOT_SLOT_COUNT; i++) { CREATURE *creature = &m_BaddieSlots[i]; creature->item_num = NO_ITEM; creature->lot.node = @@ -26,6 +26,11 @@ void LOT_InitialiseArray(void) m_SlotsUsed = 0; } +CREATURE *LOT_GetBaddieSlot(const int32_t i) +{ + return &m_BaddieSlots[i]; +} + void LOT_DisableBaddieAI(int16_t item_num) { ITEM *const item = Item_Get(item_num); @@ -43,8 +48,8 @@ bool LOT_EnableBaddieAI(const int16_t item_num, const bool always) return true; } - if (m_SlotsUsed < NUM_SLOTS) { - for (int32_t slot = 0; slot < NUM_SLOTS; slot++) { + if (m_SlotsUsed < LOT_SLOT_COUNT) { + for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) { CREATURE *creature = &m_BaddieSlots[slot]; if (creature->item_num == NO_ITEM) { LOT_InitialiseSlot(item_num, slot); @@ -64,7 +69,7 @@ bool LOT_EnableBaddieAI(const int16_t item_num, const bool always) } int32_t worst_slot = -1; - for (int32_t slot = 0; slot < NUM_SLOTS; slot++) { + for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) { CREATURE *creature = &m_BaddieSlots[slot]; const ITEM *const item = Item_Get(creature->item_num); int32_t x = (item->pos.x - g_Camera.pos.x) >> 8; diff --git a/src/tr1/game/lot.h b/src/tr1/game/lot.h index 46986c46f..8fbc00b89 100644 --- a/src/tr1/game/lot.h +++ b/src/tr1/game/lot.h @@ -7,8 +7,6 @@ #include void LOT_InitialiseArray(void); -void LOT_DisableBaddieAI(int16_t item_num); void LOT_InitialiseSlot(int16_t item_num, int32_t slot); void LOT_CreateZone(ITEM *item); void LOT_InitialiseLOT(LOT_INFO *LOT); -void LOT_ClearLOT(LOT_INFO *LOT); diff --git a/src/tr1/game/objects/common.c b/src/tr1/game/objects/common.c index e51bc1872..9e45ab52e 100644 --- a/src/tr1/game/objects/common.c +++ b/src/tr1/game/objects/common.c @@ -5,6 +5,7 @@ #include "game/lara/common.h" #include "game/objects/vars.h" #include "game/output.h" +#include "game/output/meshes/objects.h" #include "game/room.h" #include "game/viewport.h" #include "global/const.h" @@ -72,11 +73,12 @@ void Object_DrawDummyItem(const ITEM *const item) void Object_DrawSpriteItem(const ITEM *const item) { + const RGB_F tint = Output_GetTint(); Output_DrawSprite( item->interp.result.pos.x, item->interp.result.pos.y, item->interp.result.pos.z, Object_Get(item->object_id)->mesh_idx - item->frame_num, - item->shade.value_1); + item->shade.value_1 < 0 ? SHADE_NEUTRAL : item->shade.value_1, tint); } void Object_DrawPickupItem(const ITEM *const item) @@ -139,7 +141,7 @@ void Object_DrawPickupItem(const ITEM *const item) case O_SHOTGUN_OPTION: case O_MAGNUM_OPTION: case O_UZI_OPTION: - case O_MAG_AMMO_OPTION: + case O_MAGNUM_AMMO_OPTION: case O_UZI_AMMO_OPTION: case O_EXPLOSIVE_OPTION: case O_LEADBAR_OPTION: @@ -149,9 +151,9 @@ void Object_DrawPickupItem(const ITEM *const item) // Ignore the sprite and just position based upon the anim. offset = item->pos.y + (min_y - anim_y) / 2; break; - case O_MEDI_OPTION: - case O_BIGMEDI_OPTION: - case O_SG_AMMO_OPTION: + case O_SMALL_MEDIPACK_OPTION: + case O_LARGE_MEDIPACK_OPTION: + case O_SHOTGUN_AMMO_OPTION: case O_PUZZLE_OPTION_1: case O_PUZZLE_OPTION_2: case O_PUZZLE_OPTION_3: @@ -278,22 +280,19 @@ void Object_SetMeshReflective( OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + mesh_idx); mesh->enable_reflections = enabled; - for (int32_t i = 0; i < mesh->num_tex_face4s; i++) { mesh->tex_face4s[i].enable_reflections = enabled; } - for (int32_t i = 0; i < mesh->num_tex_face3s; i++) { mesh->tex_face3s[i].enable_reflections = enabled; } - for (int32_t i = 0; i < mesh->num_flat_face4s; i++) { mesh->flat_face4s[i].enable_reflections = enabled; } - for (int32_t i = 0; i < mesh->num_flat_face3s; i++) { mesh->flat_face3s[i].enable_reflections = enabled; } + Output_Meshes_ObserveObjectMeshUpdate(mesh); } void Object_SetReflective(const GAME_OBJECT_ID obj_id, const bool enabled) diff --git a/src/tr1/game/objects/creatures/ape.c b/src/tr1/game/objects/creatures/ape.c index 63728f441..90d82754b 100644 --- a/src/tr1/game/objects/creatures/ape.c +++ b/src/tr1/game/objects/creatures/ape.c @@ -44,7 +44,7 @@ typedef enum { APE_STATE_VAULT = 11, } APE_STATE; -static BITE m_ApeBite = { 0, -19, 75, 15 }; +static BITE m_ApeBite = { .pos = { 0, -19, 75 }, .mesh_num = 15 }; static bool M_Vault(int16_t item_num, int16_t angle); static void M_Setup(OBJECT *obj); @@ -127,7 +127,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 13)->rot_y = true; + Object_GetBone(obj, 13)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/baldy.c b/src/tr1/game/objects/creatures/baldy.c index 756d9810a..fa1f18d60 100644 --- a/src/tr1/game/objects/creatures/baldy.c +++ b/src/tr1/game/objects/creatures/baldy.c @@ -26,7 +26,7 @@ typedef enum { BALDY_STATE_SHOOT = 6, } BALDY_STATE; -static BITE m_BaldyGun = { -20, 440, 20, 9 }; +static BITE m_BaldyGun = { .pos = { -20, 440, 20 }, .mesh_num = 9 }; static void M_Setup(OBJECT *obj); static void M_Initialise(int16_t item_num); @@ -52,7 +52,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; } static void M_Initialise(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/bat.c b/src/tr1/game/objects/creatures/bat.c index fcbb953b2..f3cea22da 100644 --- a/src/tr1/game/objects/creatures/bat.c +++ b/src/tr1/game/objects/creatures/bat.c @@ -25,7 +25,7 @@ typedef enum { BAT_STATE_DEATH = 5, } BAT_STATE; -static BITE m_BatBite = { 0, 16, 45, 4 }; +static BITE m_BatBite = { .pos = { 0, 16, 45 }, .mesh_num = 4 }; static void M_FixEmbeddedPosition(int16_t item_num); static void M_Setup(OBJECT *obj); diff --git a/src/tr1/game/objects/creatures/centaur.c b/src/tr1/game/objects/creatures/centaur.c index b3f15e1d8..89ba04f1f 100644 --- a/src/tr1/game/objects/creatures/centaur.c +++ b/src/tr1/game/objects/creatures/centaur.c @@ -32,8 +32,8 @@ typedef enum { CENTAUR_STATE_WARNING = 6, } CENTAUR_STATE; -static BITE m_CentaurRocket = { 11, 415, 41, 13 }; -static BITE m_CentaurRear = { 50, 30, 0, 5 }; +static BITE m_CentaurRocket = { .pos = { 11, 415, 41 }, .mesh_num = 13 }; +static BITE m_CentaurRear = { .pos = { 50, 30, 0 }, .mesh_num = 5 }; static void M_Setup(OBJECT *obj); static void M_Control(int16_t item_num); @@ -57,8 +57,8 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 10)->rot_x = true; - Object_GetBone(obj, 10)->rot_y = true; + Object_GetBone(obj, 10)->rot.x = true; + Object_GetBone(obj, 10)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/cowboy.c b/src/tr1/game/objects/creatures/cowboy.c index 7c158662b..d6030d5dd 100644 --- a/src/tr1/game/objects/creatures/cowboy.c +++ b/src/tr1/game/objects/creatures/cowboy.c @@ -28,8 +28,8 @@ typedef enum { COWBOY_STATE_SHOOT = 6, } COWBOY_STATE; -static BITE m_CowboyGun1 = { 1, 200, 41, 5 }; -static BITE m_CowboyGun2 = { -2, 200, 40, 8 }; +static BITE m_CowboyGun1 = { .pos = { 1, 200, 41 }, .mesh_num = 5 }; +static BITE m_CowboyGun2 = { .pos = { -2, 200, 40 }, .mesh_num = 8 }; static void M_Setup(OBJECT *obj); static void M_HandleSave(ITEM *item, SAVEGAME_STAGE stage); @@ -54,7 +54,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) diff --git a/src/tr1/game/objects/creatures/crocodile.c b/src/tr1/game/objects/creatures/crocodile.c index 6da609a15..138949361 100644 --- a/src/tr1/game/objects/creatures/crocodile.c +++ b/src/tr1/game/objects/creatures/crocodile.c @@ -50,7 +50,7 @@ typedef enum { ALLIGATOR_STATE_DEATH = 3, } ALLIGATOR_STATE; -static BITE m_CrocodileBite = { 5, -21, 467, 9 }; +static BITE m_CrocodileBite = { .pos = { 5, -21, 467 }, .mesh_num = 9 }; static void M_SetupCrocodile(OBJECT *obj); static void M_SetupAlligator(OBJECT *obj); @@ -77,7 +77,7 @@ static void M_SetupBase(OBJECT *const obj) obj->save_hitpoints = 1; obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 7)->rot_y = true; + Object_GetBone(obj, 7)->rot.y = true; } static void M_SetupCrocodile(OBJECT *const obj) diff --git a/src/tr1/game/objects/creatures/larson.c b/src/tr1/game/objects/creatures/larson.c index 6cb37430d..47084bc1f 100644 --- a/src/tr1/game/objects/creatures/larson.c +++ b/src/tr1/game/objects/creatures/larson.c @@ -29,7 +29,7 @@ typedef enum { LARSON_STATE_SHOOT = 7, } LARSON_STATE; -static BITE m_LarsonGun = { -60, 170, 0, 14 }; +static BITE m_LarsonGun = { .pos = { -60, 170, 0 }, .mesh_num = 14 }; static void M_Setup(OBJECT *obj); static void M_HandleSave(ITEM *item, SAVEGAME_STAGE stage); @@ -54,7 +54,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 6)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) diff --git a/src/tr1/game/objects/creatures/lion.c b/src/tr1/game/objects/creatures/lion.c index 41325fe4d..d28516082 100644 --- a/src/tr1/game/objects/creatures/lion.c +++ b/src/tr1/game/objects/creatures/lion.c @@ -41,7 +41,7 @@ typedef enum { LION_STATE_ATTACK_2 = 7, } LION_STATE; -static BITE m_LionBite = { -2, -10, 132, 21 }; +static BITE m_LionBite = { .pos = { -2, -10, 132 }, .mesh_num = 21 }; static void M_SetupBase(OBJECT *obj); static void M_SetupLion(OBJECT *obj); @@ -62,7 +62,7 @@ static void M_SetupBase(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 19)->rot_y = true; + Object_GetBone(obj, 19)->rot.y = true; } static void M_SetupLion(OBJECT *const obj) diff --git a/src/tr1/game/objects/creatures/mummy.c b/src/tr1/game/objects/creatures/mummy.c index 372b18b08..05a5f0b71 100644 --- a/src/tr1/game/objects/creatures/mummy.c +++ b/src/tr1/game/objects/creatures/mummy.c @@ -37,7 +37,7 @@ static void M_Setup(OBJECT *const obj) obj->save_hitpoints = 1; obj->save_anim = 1; - Object_GetBone(obj, 2)->rot_y = true; + Object_GetBone(obj, 2)->rot.y = true; } static void M_Initialise(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/mutant.c b/src/tr1/game/objects/creatures/mutant.c index c22bc3190..3b12c474c 100644 --- a/src/tr1/game/objects/creatures/mutant.c +++ b/src/tr1/game/objects/creatures/mutant.c @@ -55,9 +55,9 @@ typedef enum { } MUTANT_STATE; static bool m_EnableExplosions = true; -static BITE m_WarriorBite = { -27, 98, 0, 10 }; -static BITE m_WarriorRocket = { 51, 213, 0, 14 }; -static BITE m_WarriorShard = { -35, 269, 0, 9 }; +static BITE m_WarriorBite = { .pos = { -27, 98, 0 }, .mesh_num = 10 }; +static BITE m_WarriorRocket = { .pos = { 51, 213, 0 }, .mesh_num = 14 }; +static BITE m_WarriorShard = { .pos = { -35, 269, 0 }, .mesh_num = 9 }; static void M_Setup(OBJECT *obj); static void M_Setup2(OBJECT *obj); @@ -83,8 +83,8 @@ static void M_Setup(OBJECT *const obj) obj->save_hitpoints = 1; obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 0)->rot_y = true; - Object_GetBone(obj, 2)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; + Object_GetBone(obj, 2)->rot.y = true; } static void M_Setup2(OBJECT *const obj) diff --git a/src/tr1/game/objects/creatures/natla.c b/src/tr1/game/objects/creatures/natla.c index 2bb873089..bf0d2c490 100644 --- a/src/tr1/game/objects/creatures/natla.c +++ b/src/tr1/game/objects/creatures/natla.c @@ -40,7 +40,7 @@ typedef enum { NATLA_STATE_DEATH = 9, } NATLA_STATE; -static BITE m_NatlaGun = { 5, 220, 7, 4 }; +static BITE m_NatlaGun = { .pos = { 5, 220, 7 }, .mesh_num = 4 }; static void M_Setup(OBJECT *obj); static void M_Control(int16_t item_num); @@ -63,8 +63,8 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 2)->rot_x = true; - Object_GetBone(obj, 2)->rot_z = true; + Object_GetBone(obj, 2)->rot.x = true; + Object_GetBone(obj, 2)->rot.z = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/pierre.c b/src/tr1/game/objects/creatures/pierre.c index 7dd4f0120..70b91d819 100644 --- a/src/tr1/game/objects/creatures/pierre.c +++ b/src/tr1/game/objects/creatures/pierre.c @@ -38,8 +38,8 @@ typedef enum { PIERRE_STATE_SHOOT = 7, } PIERRE_STATE; -static BITE m_PierreGun1 = { 60, 200, 0, 11 }; -static BITE m_PierreGun2 = { -57, 200, 0, 14 }; +static BITE m_PierreGun1 = { .pos = { 60, 200, 0 }, .mesh_num = 11 }; +static BITE m_PierreGun2 = { .pos = { -57, 200, 0 }, .mesh_num = 14 }; static int16_t m_PierreItemNum = NO_ITEM; static void M_Setup(OBJECT *obj); @@ -65,7 +65,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 6)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) diff --git a/src/tr1/game/objects/creatures/raptor.c b/src/tr1/game/objects/creatures/raptor.c index fd3845a7a..54d8f85ea 100644 --- a/src/tr1/game/objects/creatures/raptor.c +++ b/src/tr1/game/objects/creatures/raptor.c @@ -36,7 +36,7 @@ typedef enum { RAPTOR_STATE_ATTACK_3 = 8, } RAPTOR_STATE; -static BITE m_RaptorBite = { 0, 66, 318, 22 }; +static BITE m_RaptorBite = { .pos = { 0, 66, 318 }, .mesh_num = 22 }; static void M_Setup(OBJECT *obj); static void M_Control(int16_t item_num); @@ -60,7 +60,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 21)->rot_y = true; + Object_GetBone(obj, 21)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/rat.c b/src/tr1/game/objects/creatures/rat.c index b1e059132..5c68c0d0a 100644 --- a/src/tr1/game/objects/creatures/rat.c +++ b/src/tr1/game/objects/creatures/rat.c @@ -44,7 +44,7 @@ typedef enum { VOLE_STATE_DEATH = 3, } VOLE_STATE; -static BITE m_RatBite = { 0, -11, 108, 3 }; +static BITE m_RatBite = { .pos = { 0, -11, 108 }, .mesh_num = 3 }; static void M_SetupBase(OBJECT *obj); static void M_SetupRat(OBJECT *obj); @@ -75,7 +75,7 @@ static void M_SetupBase(OBJECT *const obj) obj->save_hitpoints = 1; obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 1)->rot_y = true; + Object_GetBone(obj, 1)->rot.y = true; } static void M_SetupRat(OBJECT *const obj) diff --git a/src/tr1/game/objects/creatures/skate_kid.c b/src/tr1/game/objects/creatures/skate_kid.c index 8c8d098ba..bef2fff60 100644 --- a/src/tr1/game/objects/creatures/skate_kid.c +++ b/src/tr1/game/objects/creatures/skate_kid.c @@ -34,8 +34,8 @@ typedef enum { SKATE_KID_STATE_DEATH = 5, } SKATE_KID_STATE; -static BITE m_KidGun1 = { 0, 150, 34, 7 }; -static BITE m_KidGun2 = { 0, 150, 37, 4 }; +static BITE m_KidGun1 = { .pos = { 0, 150, 34 }, .mesh_num = 7 }; +static BITE m_KidGun2 = { .pos = { 0, 150, 37 }, .mesh_num = 4 }; static void M_Setup(OBJECT *obj); static void M_Initialise(int16_t item_num); @@ -61,7 +61,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; if (!Object_Get(O_SKATEBOARD)->loaded) { LOG_WARNING( diff --git a/src/tr1/game/objects/creatures/torso.c b/src/tr1/game/objects/creatures/torso.c index 2a26e7663..597c3a647 100644 --- a/src/tr1/game/objects/creatures/torso.c +++ b/src/tr1/game/objects/creatures/torso.c @@ -70,7 +70,7 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 1)->rot_y = true; + Object_GetBone(obj, 1)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr1/game/objects/creatures/trex.c b/src/tr1/game/objects/creatures/trex.c index c253b09f1..cf1af5ab8 100644 --- a/src/tr1/game/objects/creatures/trex.c +++ b/src/tr1/game/objects/creatures/trex.c @@ -63,8 +63,8 @@ static void M_Setup(OBJECT *const obj) obj->save_anim = 1; obj->save_flags = 1; - Object_GetBone(obj, 10)->rot_y = true; - Object_GetBone(obj, 11)->rot_y = true; + Object_GetBone(obj, 10)->rot.y = true; + Object_GetBone(obj, 11)->rot.y = true; } static void M_Collision( diff --git a/src/tr1/game/objects/general/door.c b/src/tr1/game/objects/general/door.c index 81e359f13..5cbe0bfcc 100644 --- a/src/tr1/game/objects/general/door.c +++ b/src/tr1/game/objects/general/door.c @@ -1,5 +1,3 @@ -#include "game/objects/general/door.h" - #include "game/box.h" #include "game/items.h" #include "game/lara/common.h" @@ -9,6 +7,7 @@ #include #include +#include #include typedef struct { diff --git a/src/tr1/game/objects/general/door.h b/src/tr1/game/objects/general/door.h deleted file mode 100644 index c1b09abfa..000000000 --- a/src/tr1/game/objects/general/door.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "global/types.h" - -typedef enum { - DOOR_STATE_CLOSED, - DOOR_STATE_OPEN, -} DOOR_STATE; - -void Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); diff --git a/src/tr1/game/objects/general/pickup.c b/src/tr1/game/objects/general/pickup.c index 83b341eff..d9a959f84 100644 --- a/src/tr1/game/objects/general/pickup.c +++ b/src/tr1/game/objects/general/pickup.c @@ -407,11 +407,11 @@ REGISTER_OBJECT(O_SHOTGUN_ITEM, M_Setup) REGISTER_OBJECT(O_MAGNUM_ITEM, M_Setup) REGISTER_OBJECT(O_UZI_ITEM, M_Setup) REGISTER_OBJECT(O_PISTOL_AMMO_ITEM, M_Setup) -REGISTER_OBJECT(O_SG_AMMO_ITEM, M_Setup) -REGISTER_OBJECT(O_MAG_AMMO_ITEM, M_Setup) +REGISTER_OBJECT(O_SHOTGUN_AMMO_ITEM, M_Setup) +REGISTER_OBJECT(O_MAGNUM_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_UZI_AMMO_ITEM, M_Setup) REGISTER_OBJECT(O_EXPLOSIVE_ITEM, M_Setup) -REGISTER_OBJECT(O_MEDI_ITEM, M_Setup) -REGISTER_OBJECT(O_BIGMEDI_ITEM, M_Setup) +REGISTER_OBJECT(O_SMALL_MEDIPACK_ITEM, M_Setup) +REGISTER_OBJECT(O_LARGE_MEDIPACK_ITEM, M_Setup) REGISTER_OBJECT(O_LEADBAR_ITEM, M_Setup) REGISTER_OBJECT(O_SCION_ITEM_2, M_Setup) diff --git a/src/tr1/game/objects/general/trapdoor.c b/src/tr1/game/objects/general/trapdoor.c deleted file mode 100644 index a3d5728f0..000000000 --- a/src/tr1/game/objects/general/trapdoor.c +++ /dev/null @@ -1,113 +0,0 @@ -#include "game/items.h" -#include "global/const.h" - -#include - -typedef enum { - TRAPDOOR_STATE_CLOSED, - TRAPDOOR_STATE_OPEN, -} TRAPDOOR_STATE; - -static bool M_IsItemOnTop(const ITEM *item, int32_t x, int32_t z); -static void M_Setup(OBJECT *obj); -static void M_Control(int16_t item_num); -static int16_t M_GetFloorHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static int16_t M_GetCeilingHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); - -static bool M_IsItemOnTop(const ITEM *item, int32_t x, int32_t z) -{ - const BOUNDS_16 *const orig_bounds = &Item_GetBestFrame(item)->bounds; - if (orig_bounds == nullptr) { - return false; - } - - BOUNDS_16 fixed_bounds = {}; - - // Bounds need to change in order to account for 2 sector trapdoors - // and the trapdoor angle. - if (item->rot.y == 0) { - fixed_bounds.min.x = orig_bounds->min.x; - fixed_bounds.max.x = orig_bounds->max.x; - fixed_bounds.min.z = orig_bounds->min.z; - fixed_bounds.max.z = orig_bounds->max.z; - } else if (item->rot.y == DEG_90) { - fixed_bounds.min.x = orig_bounds->min.z; - fixed_bounds.max.x = orig_bounds->max.z; - fixed_bounds.min.z = -orig_bounds->max.x; - fixed_bounds.max.z = -orig_bounds->min.x; - } else if (item->rot.y == -DEG_180) { - fixed_bounds.min.x = -orig_bounds->max.x; - fixed_bounds.max.x = -orig_bounds->min.x; - fixed_bounds.min.z = -orig_bounds->max.z; - fixed_bounds.max.z = -orig_bounds->min.z; - } else if (item->rot.y == -DEG_90) { - fixed_bounds.min.x = -orig_bounds->max.z; - fixed_bounds.max.x = -orig_bounds->min.z; - fixed_bounds.min.z = orig_bounds->min.x; - fixed_bounds.max.z = orig_bounds->max.x; - } - - if (x <= item->pos.x + fixed_bounds.max.x - && x >= item->pos.x + fixed_bounds.min.x - && z <= item->pos.z + fixed_bounds.max.z - && z >= item->pos.z + fixed_bounds.min.z) { - return true; - } - - return false; -} - -static void M_Setup(OBJECT *const obj) -{ - obj->control_func = M_Control; - obj->floor_height_func = M_GetFloorHeight; - obj->ceiling_height_func = M_GetCeilingHeight; - obj->save_anim = 1; - obj->save_flags = 1; -} - -static void M_Control(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - if (Item_IsTriggerActive(item)) { - if (item->current_anim_state == TRAPDOOR_STATE_CLOSED) { - item->goal_anim_state = TRAPDOOR_STATE_OPEN; - } - } else if (item->current_anim_state == TRAPDOOR_STATE_OPEN) { - item->goal_anim_state = TRAPDOOR_STATE_CLOSED; - } - Item_Animate(item); -} - -static int16_t M_GetFloorHeight( - const ITEM *item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - if (!M_IsItemOnTop(item, x, z)) { - return height; - } - if (item->current_anim_state == TRAPDOOR_STATE_OPEN || y > item->pos.y - || item->pos.y >= height) { - return height; - } - return item->pos.y; -} - -static int16_t M_GetCeilingHeight( - const ITEM *item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - if (!M_IsItemOnTop(item, x, z)) { - return height; - } - if (item->current_anim_state == TRAPDOOR_STATE_OPEN || y <= item->pos.y - || item->pos.y <= height) { - return height; - } - return item->pos.y + STEP_L; -} - -REGISTER_OBJECT(O_TRAPDOOR_1, M_Setup) -REGISTER_OBJECT(O_TRAPDOOR_2, M_Setup) diff --git a/src/tr1/game/objects/setup.c b/src/tr1/game/objects/setup.c index 71bf60f72..e0226796a 100644 --- a/src/tr1/game/objects/setup.c +++ b/src/tr1/game/objects/setup.c @@ -27,6 +27,17 @@ static void M_SetupLaraExtra(void) obj->control_func = Lara_ControlExtra; } +static void M_SetupSkybox(void) +{ + const OBJECT *const obj = Object_Get(O_SKYBOX); + if (obj->loaded) { + for (int32_t i = 0; i < obj->mesh_count; i++) { + OBJECT_MESH *const obj_mesh = Object_GetMesh(obj->mesh_idx + i); + obj_mesh->disable_lighting = true; + } + } +} + static void M_DisableObject(const GAME_OBJECT_ID obj_id) { OBJECT *const obj = Object_Get(obj_id); @@ -67,17 +78,18 @@ void Object_SetupAllObjects(void) M_SetupLara(); M_SetupLaraExtra(); + M_SetupSkybox(); Lara_Hair_Initialise(); if (g_Config.gameplay.disable_medpacks) { - M_DisableObject(O_MEDI_ITEM); - M_DisableObject(O_BIGMEDI_ITEM); + M_DisableObject(O_SMALL_MEDIPACK_ITEM); + M_DisableObject(O_LARGE_MEDIPACK_ITEM); } if (g_Config.gameplay.disable_magnums) { M_DisableObject(O_MAGNUM_ITEM); - M_DisableObject(O_MAG_AMMO_ITEM); + M_DisableObject(O_MAGNUM_AMMO_ITEM); } if (g_Config.gameplay.disable_uzis) { @@ -87,6 +99,6 @@ void Object_SetupAllObjects(void) if (g_Config.gameplay.disable_shotgun) { M_DisableObject(O_SHOTGUN_ITEM); - M_DisableObject(O_SG_AMMO_ITEM); + M_DisableObject(O_SHOTGUN_AMMO_ITEM); } } diff --git a/src/tr1/game/objects/traps/lightning_emitter.c b/src/tr1/game/objects/traps/lightning_emitter.c index 81aed7588..345299355 100644 --- a/src/tr1/game/objects/traps/lightning_emitter.c +++ b/src/tr1/game/objects/traps/lightning_emitter.c @@ -231,11 +231,12 @@ static void M_Draw(const ITEM *const item) if (i > 0) { Output_DrawLightningSegment( - x1, y1 + l->wibble[i - 1].y, z1, x2, y2, z2, - Viewport_GetWidth() / 6); + (XYZ_32) { x1, y1 + l->wibble[i - 1].y, z1 }, + (XYZ_32) { x2, y2, z2 }, Viewport_GetWidth() / 6); } else { Output_DrawLightningSegment( - x1, y1, z1, x2, y2, z2, Viewport_GetWidth() / 6); + (XYZ_32) { x1, y1, z1 }, (XYZ_32) { x2, y2, z2 }, + Viewport_GetWidth() / 6); } x1 = x2; @@ -285,11 +286,12 @@ static void M_Draw(const ITEM *const item) if (k > 0) { Output_DrawLightningSegment( - x1, y1 + l->shoot[i][k - 1].y, z1, x2, y2, z2, - Viewport_GetWidth() / 16); + (XYZ_32) { x1, y1 + l->shoot[i][k - 1].y, z1 }, + (XYZ_32) { x2, y2, z2 }, Viewport_GetWidth() / 16); } else { Output_DrawLightningSegment( - x1, y1, z1, x2, y2, z2, Viewport_GetWidth() / 16); + (XYZ_32) { x1, y1, z1 }, (XYZ_32) { x2, y2, z2 }, + Viewport_GetWidth() / 16); } x1 = x2; diff --git a/src/tr1/game/objects/traps/movable_block.c b/src/tr1/game/objects/traps/movable_block.c index 528abb3ca..de6dd5391 100644 --- a/src/tr1/game/objects/traps/movable_block.c +++ b/src/tr1/game/objects/traps/movable_block.c @@ -278,6 +278,7 @@ static void M_Setup(OBJECT *const obj) obj->save_position = 1; obj->save_anim = 1; obj->save_flags = 1; + obj->base_rot.y = true; obj->bounds_func = M_Bounds; } @@ -290,6 +291,7 @@ static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) item->status = IS_INACTIVE; } item->priv = item->status == IS_ACTIVE ? (void *)true : (void *)false; + MovableBlock_UpdateRotation(item, item->rot.y); } } @@ -367,16 +369,16 @@ static void M_Collision( switch (quadrant) { case DIR_NORTH: - item->rot.y = 0; + MovableBlock_UpdateRotation(item, 0); break; case DIR_EAST: - item->rot.y = DEG_90; + MovableBlock_UpdateRotation(item, DEG_90); break; case DIR_SOUTH: - item->rot.y = -DEG_180; + MovableBlock_UpdateRotation(item, -DEG_180); break; case DIR_WEST: - item->rot.y = -DEG_90; + MovableBlock_UpdateRotation(item, -DEG_90); break; default: break; diff --git a/src/tr1/game/objects/traps/teeth_trap.c b/src/tr1/game/objects/traps/teeth_trap.c index 119dcbe68..fdc4ba087 100644 --- a/src/tr1/game/objects/traps/teeth_trap.c +++ b/src/tr1/game/objects/traps/teeth_trap.c @@ -12,12 +12,12 @@ typedef enum { TEETH_TRAP_STATE_NASTY = 1, } TEETH_TRAP_STATE; -static BITE m_Teeth1A = { -23, 0, -1718, 0 }; -static BITE m_Teeth1B = { 71, 0, -1718, 1 }; -static BITE m_Teeth2A = { -23, 10, -1718, 0 }; -static BITE m_Teeth2B = { 71, 10, -1718, 1 }; -static BITE m_Teeth3A = { -23, -10, -1718, 0 }; -static BITE m_Teeth3B = { 71, -10, -1718, 1 }; +static BITE m_Teeth1A = { .pos = { -23, 0, -1718 }, .mesh_num = 0 }; +static BITE m_Teeth1B = { .pos = { 71, 0, -1718 }, .mesh_num = 1 }; +static BITE m_Teeth2A = { .pos = { -23, 10, -1718 }, .mesh_num = 0 }; +static BITE m_Teeth2B = { .pos = { 71, 10, -1718 }, .mesh_num = 1 }; +static BITE m_Teeth3A = { .pos = { -23, -10, -1718 }, .mesh_num = 0 }; +static BITE m_Teeth3B = { .pos = { 71, -10, -1718 }, .mesh_num = 1 }; static void M_BiteEffect(ITEM *item, BITE *bite); static void M_Setup(OBJECT *obj); @@ -25,11 +25,7 @@ static void M_Control(int16_t item_num); static void M_BiteEffect(ITEM *item, BITE *bite) { - XYZ_32 pos = { - .x = bite->x, - .y = bite->y, - .z = bite->z, - }; + XYZ_32 pos = bite->pos; Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); Spawn_Blood(pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num); } diff --git a/src/tr1/game/objects/vars.c b/src/tr1/game/objects/vars.c index 12083270f..dd2b85e40 100644 --- a/src/tr1/game/objects/vars.c +++ b/src/tr1/game/objects/vars.c @@ -83,11 +83,11 @@ const GAME_OBJECT_ID g_PickupObjects[] = { O_MAGNUM_ITEM, O_UZI_ITEM, O_PISTOL_AMMO_ITEM, - O_SG_AMMO_ITEM, - O_MAG_AMMO_ITEM, + O_SHOTGUN_AMMO_ITEM, + O_MAGNUM_AMMO_ITEM, O_UZI_AMMO_ITEM, - O_MEDI_ITEM, - O_BIGMEDI_ITEM, + O_SMALL_MEDIPACK_ITEM, + O_LARGE_MEDIPACK_ITEM, O_EXPLOSIVE_ITEM, O_PUZZLE_ITEM_1, O_PUZZLE_ITEM_2, @@ -132,9 +132,9 @@ const GAME_OBJECT_ID g_DoorObjects[] = { const GAME_OBJECT_ID g_TrapdoorObjects[] = { // clang-format off - O_TRAPDOOR_1, - O_TRAPDOOR_2, - O_BIGTRAPDOOR, + O_TRAPDOOR_TYPE_1, + O_TRAPDOOR_TYPE_2, + O_TRAPDOOR_TYPE_3, O_DRAWBRIDGE, NO_OBJECT, // clang-format on @@ -187,12 +187,12 @@ const GAME_OBJECT_ID g_InvObjects[] = { O_SHOTGUN_OPTION, O_MAGNUM_OPTION, O_UZI_OPTION, - O_SG_AMMO_OPTION, - O_MAG_AMMO_OPTION, + O_SHOTGUN_AMMO_OPTION, + O_MAGNUM_AMMO_OPTION, O_UZI_AMMO_OPTION, O_EXPLOSIVE_OPTION, - O_MEDI_OPTION, - O_BIGMEDI_OPTION, + O_SMALL_MEDIPACK_OPTION, + O_LARGE_MEDIPACK_OPTION, O_PUZZLE_OPTION_1, O_PUZZLE_OPTION_2, O_PUZZLE_OPTION_3, @@ -216,11 +216,22 @@ const GAME_OBJECT_ID g_InvObjects[] = { // clang-format on }; +const GAME_OBJECT_ID g_WaterSpriteObjects[] = { + // clang-format off + O_WATERFALL, + O_SPLASH_1, + O_SPLASH_2, + O_BUBBLES_1, + O_BUBBLES_2, + NO_OBJECT, + // clang-format on +}; + const GAME_OBJECT_PAIR g_GunAmmoObjectMap[] = { // clang-format off { O_PISTOL_ITEM, O_PISTOL_AMMO_ITEM }, - { O_SHOTGUN_ITEM, O_SG_AMMO_ITEM }, - { O_MAGNUM_ITEM, O_MAG_AMMO_ITEM }, + { O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM }, + { O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM }, { O_UZI_ITEM, O_UZI_AMMO_ITEM }, { NO_OBJECT, NO_OBJECT }, // clang-format on @@ -233,12 +244,12 @@ const GAME_OBJECT_PAIR g_ItemToInvObjectMap[] = { { O_MAGNUM_ITEM, O_MAGNUM_OPTION }, { O_UZI_ITEM, O_UZI_OPTION }, { O_PISTOL_AMMO_ITEM, O_PISTOL_AMMO_OPTION }, - { O_SG_AMMO_ITEM, O_SG_AMMO_OPTION }, - { O_MAG_AMMO_ITEM, O_MAG_AMMO_OPTION }, + { O_SHOTGUN_AMMO_ITEM, O_SHOTGUN_AMMO_OPTION }, + { O_MAGNUM_AMMO_ITEM, O_MAGNUM_AMMO_OPTION }, { O_UZI_AMMO_ITEM, O_UZI_AMMO_OPTION }, { O_EXPLOSIVE_ITEM, O_EXPLOSIVE_OPTION }, - { O_MEDI_ITEM, O_MEDI_OPTION }, - { O_BIGMEDI_ITEM, O_BIGMEDI_OPTION }, + { O_SMALL_MEDIPACK_ITEM, O_SMALL_MEDIPACK_OPTION }, + { O_LARGE_MEDIPACK_ITEM, O_LARGE_MEDIPACK_OPTION }, { O_PUZZLE_ITEM_1, O_PUZZLE_OPTION_1 }, { O_PUZZLE_ITEM_2, O_PUZZLE_OPTION_2 }, { O_PUZZLE_ITEM_3, O_PUZZLE_OPTION_3 }, diff --git a/src/tr1/game/option/option.c b/src/tr1/game/option/option.c index ca2d2604e..957cc80d6 100644 --- a/src/tr1/game/option/option.c +++ b/src/tr1/game/option/option.c @@ -104,16 +104,16 @@ void Option_Control(INVENTORY_ITEM *inv_item, const bool is_busy) case O_MAGNUM_OPTION: case O_UZI_OPTION: case O_EXPLOSIVE_OPTION: - case O_MEDI_OPTION: - case O_BIGMEDI_OPTION: + case O_SMALL_MEDIPACK_OPTION: + case O_LARGE_MEDIPACK_OPTION: if (!is_busy) { g_InputDB.menu_confirm = 1; } break; case O_PISTOL_AMMO_OPTION: - case O_SG_AMMO_OPTION: - case O_MAG_AMMO_OPTION: + case O_SHOTGUN_AMMO_OPTION: + case O_MAGNUM_AMMO_OPTION: case O_UZI_AMMO_OPTION: break; @@ -145,9 +145,13 @@ void Option_Control(INVENTORY_ITEM *inv_item, const bool is_busy) } } -void Option_Draw(INVENTORY_ITEM *inv_item) +void Option_Draw(INVENTORY_ITEM *const inv_item) { switch (inv_item->object_id) { + case O_PASSPORT_OPTION: + Option_Passport_Draw(inv_item); + break; + case O_CONTROL_OPTION: switch (m_ControlMode) { case CM_KEYBOARD: diff --git a/src/tr1/game/option/option_compass.c b/src/tr1/game/option/option_compass.c index d38e3c010..ed1be4085 100644 --- a/src/tr1/game/option/option_compass.c +++ b/src/tr1/game/option/option_compass.c @@ -5,74 +5,80 @@ #include "game/game_string.h" #include "game/input.h" #include "game/text.h" -#include "game/ui/widgets/stats_dialog.h" #include "global/vars.h" #include +#include #include #include -static UI_WIDGET *m_Dialog = nullptr; +typedef struct { + struct { + bool is_ready; + UI_STATS_DIALOG_STATE state; + } ui; +} M_PRIV; + +static M_PRIV m_Priv = {}; + static int16_t m_CompassNeedle = 0; static int16_t m_CompassSpeed = 0; -static void M_Init(void); -static void M_Shutdown(void); +static void M_Init(M_PRIV *p); +static void M_Shutdown(M_PRIV *p); -static void M_Init(void) +static void M_Init(M_PRIV *const p) { - m_Dialog = UI_StatsDialog_Create((UI_STATS_DIALOG_ARGS) { - .mode = UI_STATS_DIALOG_MODE_LEVEL, - .style = UI_STATS_DIALOG_STYLE_BORDERED, - .level_num = Game_GetCurrentLevel()->num, - }); + p->ui.is_ready = true; + UI_StatsDialog_Init( + &p->ui.state, + (UI_STATS_DIALOG_ARGS) { + .mode = UI_STATS_DIALOG_MODE_LEVEL, + .style = UI_STATS_DIALOG_STYLE_BORDERED, + .level_num = Game_GetCurrentLevel()->num, + }); } -static void M_Shutdown(void) +static void M_Shutdown(M_PRIV *const p) { - if (m_Dialog != nullptr) { - m_Dialog->free(m_Dialog); - m_Dialog = nullptr; + if (p->ui.is_ready) { + p->ui.is_ready = false; + UI_StatsDialog_Free(&p->ui.state); } } void Option_Compass_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { + M_PRIV *const p = &m_Priv; if (is_busy) { return; } - if (g_Config.gameplay.enable_compass_stats) { - char buf[100]; - char time_buf[100]; - - if (m_Dialog == nullptr) { - M_Init(); - } - - if (m_Dialog != nullptr) { - m_Dialog->control(m_Dialog); - } + if (!p->ui.is_ready && g_Config.gameplay.enable_compass_stats) { + M_Init(p); } + UI_StatsDialog_Control(&p->ui.state); if (g_InputDB.menu_confirm || g_InputDB.menu_back) { - M_Shutdown(); - inv_item->goal_frame = inv_item->frames_total - 1; + M_Shutdown(p); inv_item->anim_direction = 1; + inv_item->goal_frame = inv_item->frames_total - 1; } } void Option_Compass_Draw(void) { - if (m_Dialog != nullptr) { - m_Dialog->draw(m_Dialog); + M_PRIV *const p = &m_Priv; + if (p->ui.is_ready) { + UI_StatsDialog(&p->ui.state); } } void Option_Compass_Shutdown(void) { - M_Shutdown(); + M_PRIV *const p = &m_Priv; + M_Shutdown(p); } void Option_Compass_UpdateNeedle(const INVENTORY_ITEM *const inv_item) diff --git a/src/tr1/game/option/option_controls.c b/src/tr1/game/option/option_controls.c index 566991fa8..fdc60db28 100644 --- a/src/tr1/game/option/option_controls.c +++ b/src/tr1/game/option/option_controls.c @@ -295,10 +295,10 @@ static void M_InitText(INPUT_BACKEND backend, INPUT_LAYOUT layout) Text_AddOutline(m_Text[TEXT_TITLE_BORDER], TS_BACKGROUND); sprintf( - m_ResetGS, GS(CONTROL_RESET_DEFAULTS), + m_ResetGS, GS(CONTROLS_RESET_DEFAULTS), Input_GetKeyName(backend, layout, INPUT_ROLE_RESET_BINDINGS)); sprintf( - m_UnbindGS, GS(CONTROL_UNBIND), + m_UnbindGS, GS(CONTROLS_UNBIND), Input_GetKeyName(backend, layout, INPUT_ROLE_UNBIND_KEY)); m_Text[TEXT_RESET] = @@ -340,11 +340,11 @@ static void M_UpdateText(INPUT_BACKEND backend, INPUT_LAYOUT layout) } sprintf( - m_ResetGS, GS(CONTROL_RESET_DEFAULTS), + m_ResetGS, GS(CONTROLS_RESET_DEFAULTS), Input_GetKeyName(backend, layout, INPUT_ROLE_RESET_BINDINGS)); Text_ChangeText(m_Text[TEXT_RESET], m_ResetGS); sprintf( - m_UnbindGS, GS(CONTROL_UNBIND), + m_UnbindGS, GS(CONTROLS_UNBIND), Input_GetKeyName(backend, layout, INPUT_ROLE_UNBIND_KEY)); Text_ChangeText(m_Text[TEXT_UNBIND], m_UnbindGS); @@ -507,10 +507,10 @@ static void M_ProgressBar( const TEXTSTRING *const txt, BAR_INFO *const bar, const int32_t timer) { int32_t width = Text_GetWidth(txt); - int32_t height = TEXT_HEIGHT; + int32_t height = TEXT_HEIGHT_FIXED * 2 / 3; int32_t x = txt->pos.x; - int32_t y = txt->pos.y - TEXT_HEIGHT; + int32_t y = txt->pos.y - height; if (txt->flags.centre_h) { x += (Screen_GetResWidthDownscaled(RSR_TEXT) - width) / 2; diff --git a/src/tr1/game/option/option_controls_pick.c b/src/tr1/game/option/option_controls_pick.c index 6b887371b..746debb0d 100644 --- a/src/tr1/game/option/option_controls_pick.c +++ b/src/tr1/game/option/option_controls_pick.c @@ -29,13 +29,13 @@ static void M_InitText(void) Text_AddBackground(m_Text[TEXT_TITLE_BORDER], 180, 85, 0, 0, TS_BACKGROUND); Text_AddOutline(m_Text[TEXT_TITLE_BORDER], TS_BACKGROUND); - m_Text[TEXT_TITLE] = Text_Create(0, -30, GS(CONTROL_CUSTOMIZE)); + m_Text[TEXT_TITLE] = Text_Create(0, -30, GS(CONTROLS_CUSTOMIZE)); Text_AddBackground(m_Text[TEXT_TITLE], 176, 0, 0, 0, TS_HEADING); Text_AddOutline(m_Text[TEXT_TITLE], TS_HEADING); - m_Text[TEXT_KEYBOARD] = Text_Create(0, 0, GS(CONTROL_BACKEND_KEYBOARD)); + m_Text[TEXT_KEYBOARD] = Text_Create(0, 0, GS(CONTROLS_BACKEND_KEYBOARD)); m_Text[TEXT_CONTROLLER] = - Text_Create(0, 25, GS(CONTROL_BACKEND_CONTROLLER)); + Text_Create(0, 25, GS(CONTROLS_BACKEND_CONTROLLER)); Text_AddBackground(m_Text[g_OptionSelected], 128, 0, 0, 0, TS_REQUESTED); Text_AddOutline(m_Text[g_OptionSelected], TS_REQUESTED); diff --git a/src/tr1/game/option/option_examine.c b/src/tr1/game/option/option_examine.c index cba8e27af..924ab593e 100644 --- a/src/tr1/game/option/option_examine.c +++ b/src/tr1/game/option/option_examine.c @@ -1,21 +1,38 @@ #include "game/option/option_examine.h" #include "game/input.h" -#include "game/ui/widgets/paginator.h" #include -#include +#include #define MAX_LINES 10 -static UI_WIDGET *m_PaginatorUI = nullptr; +typedef struct { + struct { + bool is_ready; + UI_EXAMINE_ITEM_STATE state; + } ui; +} M_PRIV; -static void M_End(void); +static M_PRIV m_Priv = {}; -static void M_End(void) +static void M_Init(M_PRIV *p, GAME_OBJECT_ID obj_id); +static void M_Shutdown(M_PRIV *p); + +static void M_Init(M_PRIV *const p, const GAME_OBJECT_ID obj_id) { - m_PaginatorUI->free(m_PaginatorUI); - m_PaginatorUI = nullptr; + p->ui.is_ready = true; + UI_ExamineItem_Init( + &p->ui.state, Object_GetName(obj_id), Object_GetDescription(obj_id), + MAX_LINES); +} + +static void M_Shutdown(M_PRIV *const p) +{ + if (p->ui.is_ready) { + UI_ExamineItem_Free(&p->ui.state); + p->ui.is_ready = false; + } } bool Option_Examine_CanExamine(const GAME_OBJECT_ID obj_id) @@ -25,37 +42,37 @@ bool Option_Examine_CanExamine(const GAME_OBJECT_ID obj_id) bool Option_Examine_IsActive(void) { - return m_PaginatorUI != nullptr; + const M_PRIV *const p = &m_Priv; + return p->ui.is_ready; } void Option_Examine_Control(const GAME_OBJECT_ID obj_id, const bool is_busy) { + M_PRIV *const p = &m_Priv; if (is_busy) { return; } - if (m_PaginatorUI == nullptr) { - m_PaginatorUI = UI_Paginator_Create( - Object_GetName(obj_id), Object_GetDescription(obj_id), MAX_LINES); + if (!p->ui.is_ready) { + M_Init(p, obj_id); } - - m_PaginatorUI->control(m_PaginatorUI); + UI_ExamineItem_Control(&p->ui.state); if (g_InputDB.menu_back || g_InputDB.menu_confirm) { - M_End(); + M_Shutdown(p); } } void Option_Examine_Draw(void) { - if (m_PaginatorUI != nullptr) { - m_PaginatorUI->draw(m_PaginatorUI); + M_PRIV *const p = &m_Priv; + if (p->ui.is_ready) { + UI_ExamineItem(&p->ui.state); } } void Option_Examine_Shutdown(void) { - if (m_PaginatorUI != nullptr) { - M_End(); - } + M_PRIV *const p = &m_Priv; + M_Shutdown(p); } diff --git a/src/tr1/game/option/option_graphics.c b/src/tr1/game/option/option_graphics.c index 32eefce96..dbede5a5b 100644 --- a/src/tr1/game/option/option_graphics.c +++ b/src/tr1/game/option/option_graphics.c @@ -24,6 +24,7 @@ #define LEFT_ARROW_OFFSET (-20) #define RIGHT_ARROW_OFFSET_MIN 35 #define RIGHT_ARROW_OFFSET_MAX 85 +#define COLOR_SHIFT 10 typedef enum { TEXT_TITLE, @@ -38,6 +39,11 @@ typedef enum { typedef enum { OPTION_FPS, + OPTION_FOG_START, + OPTION_FOG_END, + OPTION_WATER_COLOR_R, + OPTION_WATER_COLOR_G, + OPTION_WATER_COLOR_B, OPTION_TEXTURE_FILTER, OPTION_FBO_FILTER, OPTION_VSYNC, @@ -47,7 +53,6 @@ typedef enum { OPTION_RENDER_MODE, OPTION_RESOLUTION, OPTION_TRAPEZOID_FILTER, - OPTION_PRETTY_PIXELS, OPTION_REFLECTIONS, OPTION_NUMBER_OF, OPTION_MIN = OPTION_FPS, @@ -73,7 +78,15 @@ typedef struct { } GRAPHICS_MENU; static const GRAPHICS_OPTION_ROW m_GfxOptionRows[] = { - { OPTION_FPS, GS_ID(DETAIL_FPS), GS_ID(DETAIL_DECIMAL_FMT) }, + { OPTION_FPS, GS_ID(DETAIL_FPS), GS_ID(DETAIL_INTEGER_FMT) }, + { OPTION_FOG_START, GS_ID(DETAIL_FOG_START), GS_ID(DETAIL_INTEGER_FMT) }, + { OPTION_FOG_END, GS_ID(DETAIL_FOG_END), GS_ID(DETAIL_INTEGER_FMT) }, + { OPTION_WATER_COLOR_R, GS_ID(DETAIL_WATER_COLOR_R), + GS_ID(DETAIL_INTEGER_FMT) }, + { OPTION_WATER_COLOR_G, GS_ID(DETAIL_WATER_COLOR_G), + GS_ID(DETAIL_INTEGER_FMT) }, + { OPTION_WATER_COLOR_B, GS_ID(DETAIL_WATER_COLOR_B), + GS_ID(DETAIL_INTEGER_FMT) }, { OPTION_TEXTURE_FILTER, GS_ID(DETAIL_TEXTURE_FILTER), GS_ID(MISC_OFF) }, { OPTION_FBO_FILTER, GS_ID(DETAIL_FBO_FILTER), GS_ID(MISC_OFF) }, { OPTION_VSYNC, GS_ID(DETAIL_VSYNC), GS_ID(MISC_ON) }, @@ -86,7 +99,6 @@ static const GRAPHICS_OPTION_ROW m_GfxOptionRows[] = { { OPTION_RESOLUTION, GS_ID(DETAIL_RESOLUTION), GS_ID(DETAIL_RESOLUTION_FMT) }, { OPTION_TRAPEZOID_FILTER, GS_ID(DETAIL_TRAPEZOID_FILTER), GS_ID(MISC_ON) }, - { OPTION_PRETTY_PIXELS, GS_ID(DETAIL_PRETTY_PIXELS), GS_ID(MISC_ON) }, { OPTION_REFLECTIONS, GS_ID(DETAIL_REFLECTIONS), GS_ID(MISC_ON) }, // end { OPTION_NUMBER_OF, 0, 0 }, @@ -191,7 +203,7 @@ static void M_InitText(void) Text_CentreH(m_Text[TEXT_TITLE_BORDER], 1); Text_CentreV(m_Text[TEXT_TITLE_BORDER], 1); - m_Text[TEXT_TITLE] = Text_Create(0, TOP_Y, GS(DETAIL_SELECT_DETAIL)); + m_Text[TEXT_TITLE] = Text_Create(0, TOP_Y, GS(DETAIL_TITLE)); Text_CentreH(m_Text[TEXT_TITLE], 1); Text_CentreV(m_Text[TEXT_TITLE], 1); Text_AddBackground(m_Text[TEXT_TITLE], ROW_WIDTH - 4, 0, 0, 0, TS_HEADING); @@ -267,6 +279,26 @@ static void M_UpdateArrows( m_HideArrowLeft = g_Config.rendering.fps == 30; m_HideArrowRight = g_Config.rendering.fps == 60; break; + case OPTION_FOG_START: + m_HideArrowLeft = g_Config.visuals.fog_start <= 1; + m_HideArrowRight = g_Config.visuals.fog_start >= 100; + break; + case OPTION_FOG_END: + m_HideArrowLeft = g_Config.visuals.fog_end <= 1; + m_HideArrowRight = g_Config.visuals.fog_end >= 100; + break; + case OPTION_WATER_COLOR_R: + m_HideArrowLeft = g_Config.visuals.water_color.r <= 0; + m_HideArrowRight = g_Config.visuals.water_color.r >= 255; + break; + case OPTION_WATER_COLOR_G: + m_HideArrowLeft = g_Config.visuals.water_color.g <= 0; + m_HideArrowRight = g_Config.visuals.water_color.g >= 255; + break; + case OPTION_WATER_COLOR_B: + m_HideArrowLeft = g_Config.visuals.water_color.b <= 0; + m_HideArrowRight = g_Config.visuals.water_color.b >= 255; + break; case OPTION_TEXTURE_FILTER: m_HideArrowLeft = g_Config.rendering.texture_filter == GFX_TF_FIRST; m_HideArrowRight = g_Config.rendering.texture_filter == GFX_TF_LAST; @@ -305,10 +337,6 @@ static void M_UpdateArrows( m_HideArrowLeft = !g_Config.rendering.enable_trapezoid_filter; m_HideArrowRight = g_Config.rendering.enable_trapezoid_filter; break; - case OPTION_PRETTY_PIXELS: - m_HideArrowLeft = !g_Config.rendering.pretty_pixels; - m_HideArrowRight = g_Config.rendering.pretty_pixels; - break; case OPTION_REFLECTIONS: m_HideArrowLeft = !g_Config.visuals.enable_reflections; m_HideArrowRight = g_Config.visuals.enable_reflections; @@ -387,7 +415,32 @@ static void M_ChangeTextOption( switch (row->option_name) { case OPTION_FPS: - sprintf(buf, GS(DETAIL_DECIMAL_FMT), g_Config.rendering.fps); + sprintf(buf, GS(DETAIL_INTEGER_FMT), g_Config.rendering.fps); + Text_ChangeText(value_text, buf); + break; + + case OPTION_FOG_START: + sprintf(buf, GS(DETAIL_INTEGER_FMT), g_Config.visuals.fog_start); + Text_ChangeText(value_text, buf); + break; + + case OPTION_FOG_END: + sprintf(buf, GS(DETAIL_INTEGER_FMT), g_Config.visuals.fog_end); + Text_ChangeText(value_text, buf); + break; + + case OPTION_WATER_COLOR_R: + sprintf(buf, GS(DETAIL_INTEGER_FMT), g_Config.visuals.water_color.r); + Text_ChangeText(value_text, buf); + break; + + case OPTION_WATER_COLOR_G: + sprintf(buf, GS(DETAIL_INTEGER_FMT), g_Config.visuals.water_color.g); + Text_ChangeText(value_text, buf); + break; + + case OPTION_WATER_COLOR_B: + sprintf(buf, GS(DETAIL_INTEGER_FMT), g_Config.visuals.water_color.b); Text_ChangeText(value_text, buf); break; @@ -452,12 +505,6 @@ static void M_ChangeTextOption( break; } - case OPTION_PRETTY_PIXELS: { - bool is_enabled = g_Config.rendering.pretty_pixels; - Text_ChangeText(value_text, is_enabled ? GS(MISC_ON) : GS(MISC_OFF)); - break; - } - case OPTION_REFLECTIONS: { bool is_enabled = g_Config.visuals.enable_reflections; Text_ChangeText(value_text, is_enabled ? GS(MISC_ON) : GS(MISC_OFF)); @@ -497,6 +544,7 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) int32_t reset = -1; + const int32_t color_shift = g_Input.slow ? 1 : COLOR_SHIFT; if (g_InputDB.menu_right) { switch (m_GraphicsMenu.cur_option->option_name) { case OPTION_FPS: @@ -504,6 +552,43 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) reset = OPTION_FPS; break; + case OPTION_FOG_START: + g_Config.visuals.fog_start++; + reset = OPTION_FOG_START; + break; + + case OPTION_FOG_END: + g_Config.visuals.fog_end++; + reset = OPTION_FOG_END; + break; + + case OPTION_WATER_COLOR_R: + if (g_Config.visuals.water_color.r < 255 - color_shift) { + g_Config.visuals.water_color.r += color_shift; + } else { + g_Config.visuals.water_color.r = 255; + } + reset = OPTION_WATER_COLOR_R; + break; + + case OPTION_WATER_COLOR_G: + if (g_Config.visuals.water_color.g < 255 - color_shift) { + g_Config.visuals.water_color.g += color_shift; + } else { + g_Config.visuals.water_color.g = 255; + } + reset = OPTION_WATER_COLOR_G; + break; + + case OPTION_WATER_COLOR_B: + if (g_Config.visuals.water_color.b < 255 - color_shift) { + g_Config.visuals.water_color.b += color_shift; + } else { + g_Config.visuals.water_color.b = 255; + } + reset = OPTION_WATER_COLOR_B; + break; + case OPTION_TEXTURE_FILTER: if (g_Config.rendering.texture_filter != GFX_TF_LAST) { g_Config.rendering.texture_filter++; @@ -576,13 +661,6 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) } break; - case OPTION_PRETTY_PIXELS: - if (!g_Config.rendering.pretty_pixels) { - g_Config.rendering.pretty_pixels = true; - reset = OPTION_PRETTY_PIXELS; - } - break; - case OPTION_REFLECTIONS: if (!g_Config.visuals.enable_reflections) { g_Config.visuals.enable_reflections = true; @@ -603,6 +681,44 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) reset = OPTION_FPS; break; + case OPTION_FOG_START: + g_Config.visuals.fog_start--; + reset = OPTION_FOG_START; + break; + + case OPTION_FOG_END: + g_Config.visuals.fog_end--; + reset = OPTION_FOG_END; + break; + + case OPTION_WATER_COLOR_R: + if (g_Config.visuals.water_color.r >= color_shift) { + g_Config.visuals.water_color.r -= color_shift; + } else { + g_Config.visuals.water_color.r = 0; + } + reset = OPTION_WATER_COLOR_R; + break; + + case OPTION_WATER_COLOR_G: + if (g_Config.visuals.water_color.g >= color_shift) { + g_Config.visuals.water_color.g -= color_shift; + } else { + g_Config.visuals.water_color.g = 0; + } + reset = OPTION_WATER_COLOR_G; + break; + + case OPTION_WATER_COLOR_B: + if (g_Config.visuals.water_color.b >= color_shift) { + g_Config.visuals.water_color.b -= color_shift; + } else { + g_Config.visuals.water_color.b = 0; + } + CLAMP(g_Config.visuals.water_color.b, 0, 255); + reset = OPTION_WATER_COLOR_B; + break; + case OPTION_TEXTURE_FILTER: if (g_Config.rendering.texture_filter != GFX_TF_FIRST) { g_Config.rendering.texture_filter--; @@ -675,13 +791,6 @@ void Option_Graphics_Control(INVENTORY_ITEM *inv_item, const bool is_busy) } break; - case OPTION_PRETTY_PIXELS: - if (g_Config.rendering.pretty_pixels) { - g_Config.rendering.pretty_pixels = false; - reset = OPTION_PRETTY_PIXELS; - } - break; - case OPTION_REFLECTIONS: if (g_Config.visuals.enable_reflections) { g_Config.visuals.enable_reflections = false; diff --git a/src/tr1/game/option/option_passport.c b/src/tr1/game/option/option_passport.c index 144b4c2c7..baee61364 100644 --- a/src/tr1/game/option/option_passport.c +++ b/src/tr1/game/option/option_passport.c @@ -4,8 +4,8 @@ #include "game/game_flow.h" #include "game/game_string.h" #include "game/input.h" +#include "game/inventory.h" #include "game/inventory_ring.h" -#include "game/requester.h" #include "game/savegame.h" #include "game/screen.h" #include "game/sound.h" @@ -14,6 +14,7 @@ #include "global/vars.h" #include +#include #include #include @@ -26,8 +27,6 @@ typedef enum { TEXT_PAGE_NAME = 0, TEXT_LEFT_ARROW = 1, TEXT_RIGHT_ARROW = 2, - TEXT_LEVEL_ARROW_RIGHT = 3, - TEXT_LEVEL_ARROW_LEFT = 4, TEXT_NUMBER_OF = 5, } PASSPORT_TEXT; @@ -49,6 +48,17 @@ static struct { M_PAGE pages[PAGE_COUNT]; M_PAGE_NUMBER current_page; M_PAGE_NUMBER active_page; + bool is_ready; + struct { + bool is_ready; + UI_NEW_GAME_STATE state; + } new_game; + struct { + UI_SELECT_LEVEL_DIALOG_STATE *state; + } select_level; + struct { + UI_SAVE_SLOT_DIALOG_STATE *state; + } save_slot; } m_State = { .current_page = PAGE_1, .active_page = -1, @@ -63,54 +73,6 @@ static struct { static bool m_IsTextInit = false; static TEXTSTRING *m_Text[TEXT_NUMBER_OF] = {}; -static REQUEST_INFO m_NewGameRequester = { - .items_used = 0, - .max_items = MAX_GAME_MODES, - .requested = 0, - .vis_lines = MAX_GAME_MODES, - .line_offset = 0, - .line_old_offset = 0, - .pix_width = 162, - .line_height = TEXT_HEIGHT + 7, - .is_blockable = false, - .x = 0, - .y = 0, - .heading_text = nullptr, - .items = nullptr, -}; - -static REQUEST_INFO m_SelectLevelRequester = { - .items_used = 0, - .max_items = 2, - .requested = 0, - .vis_lines = -1, - .line_offset = 0, - .line_old_offset = 0, - .pix_width = 292, - .line_height = TEXT_HEIGHT + 7, - .is_blockable = false, - .x = 0, - .y = -32, - .heading_text = nullptr, - .items = nullptr, -}; - -REQUEST_INFO g_SavegameRequester = { - .items_used = 0, - .max_items = 1, - .requested = 0, - .vis_lines = -1, - .line_offset = 0, - .line_old_offset = 0, - .pix_width = 292, - .line_height = TEXT_HEIGHT + 7, - .is_blockable = false, - .x = 0, - .y = -32, - .heading_text = nullptr, - .items = nullptr, -}; - static void M_InitRequesters(void); static void M_InitText(void); static void M_RemoveAllText(void); @@ -120,9 +82,7 @@ static void M_ChangePageTextContent(const char *text); static void M_SetPage(int32_t page, PASSPORT_MODE role, bool available); static void M_DeterminePages(void); static void M_InitSaveRequester(int16_t page_num); -static void M_RestoreSaveRequester(void); static void M_InitSelectLevelRequester(void); -static void M_InitNewGameRequester(void); static void M_ShowSaves(PASSPORT_MODE pending_mode); static void M_ShowSelectLevel(void); static void M_LoadGame(void); @@ -135,15 +95,16 @@ static void M_FlipLeft(INVENTORY_ITEM *inv_item); static void M_ShowPage(INVENTORY_ITEM *inv_item); static void M_HandleFlipInputs(void); -void M_InitRequesters(void) +static void M_InitRequesters(void) { - Requester_Shutdown(&m_SelectLevelRequester); - Requester_Shutdown(&m_NewGameRequester); - Requester_Shutdown(&g_SavegameRequester); - Requester_Init(&g_SavegameRequester, Savegame_GetSlotCount()); - Requester_Init( - &m_SelectLevelRequester, g_GameFlow.level_tables[GFLT_MAIN].count + 1); - Requester_Init(&m_NewGameRequester, MAX_GAME_MODES); + UI_NewGame_Init(&m_State.new_game.state); +} + +static void M_FreeRequesters(void) +{ + UI_NewGame_Free(&m_State.new_game.state); + m_State.new_game.is_ready = false; + m_State.is_ready = false; } static void M_InitText(void) @@ -154,12 +115,6 @@ static void M_InitText(void) m_Text[TEXT_RIGHT_ARROW] = Text_Create(70, -15, "\\{button right}"); Text_Hide(m_Text[TEXT_RIGHT_ARROW], true); - m_Text[TEXT_LEVEL_ARROW_LEFT] = Text_Create(0, 0, "\\{button left}"); - Text_Hide(m_Text[TEXT_LEVEL_ARROW_LEFT], true); - - m_Text[TEXT_LEVEL_ARROW_RIGHT] = Text_Create(0, 0, "\\{button right}"); - Text_Hide(m_Text[TEXT_LEVEL_ARROW_RIGHT], true); - m_Text[TEXT_PAGE_NAME] = Text_Create(0, -16, ""); for (int i = 0; i < TEXT_NUMBER_OF; i++) { @@ -174,9 +129,15 @@ static void M_RemoveAllText(void) Text_Remove(m_Text[i]); m_Text[i] = nullptr; } - Requester_Shutdown(&m_SelectLevelRequester); - Requester_Shutdown(&m_NewGameRequester); - Requester_Shutdown(&g_SavegameRequester); + if (m_State.select_level.state != nullptr) { + UI_SelectLevelDialog_Free(m_State.select_level.state); + m_State.select_level.state = nullptr; + } + if (m_State.save_slot.state != nullptr) { + UI_SaveSlotDialog_Free(m_State.save_slot.state); + m_State.save_slot.state = nullptr; + } + M_FreeRequesters(); } static void M_Close(INVENTORY_ITEM *inv_item) @@ -229,9 +190,10 @@ static void M_SetPage( static void M_DeterminePages(void) { - const bool has_saves = g_SavedGamesCount > 0 && Savegame_GetSlotCount() > 0; + const bool has_saves = + Savegame_GetTotalCount() > 0 && Savegame_GetSlotCount() > 0; - switch (g_InvMode) { + switch (g_Inv_Mode) { case INV_TITLE_MODE: m_State.mode = PASSPORT_MODE_BROWSE; M_SetPage(PAGE_1, PASSPORT_MODE_LOAD_GAME, has_saves); @@ -259,7 +221,7 @@ static void M_DeterminePages(void) M_SetPage(PAGE_2, PASSPORT_MODE_UNAVAILABLE, false); M_SetPage(PAGE_3, PASSPORT_MODE_UNAVAILABLE, false); if (m_State.mode == PASSPORT_MODE_RESTART) { - M_InitNewGameRequester(); + m_State.new_game.is_ready = true; } else { M_InitSaveRequester(PAGE_1); } @@ -315,134 +277,76 @@ static void M_DeterminePages(void) } } -static void M_InitSaveRequester(int16_t page_num) +static void M_InitSaveRequester(const int16_t page_num) { - REQUEST_INFO *req = &g_SavegameRequester; - Requester_ClearTextstrings(req); - Requester_SetHeading( - req, - page_num == PAGE_1 ? GS(PASSPORT_LOAD_GAME) : GS(PASSPORT_SAVE_GAME)); - - if (Screen_GetResHeightDownscaled(RSR_TEXT) <= 240) { - req->vis_lines = 5; - } else if (Screen_GetResHeightDownscaled(RSR_TEXT) <= 384) { - req->vis_lines = 7; - } else if (Screen_GetResHeightDownscaled(RSR_TEXT) <= 480) { - req->vis_lines = 10; - } else { - req->vis_lines = 12; + int32_t save_slot = g_GameInfo.select_save_slot; + if (save_slot == -1) { + save_slot = Savegame_GetMostRecentlyUsedSlot(); } - req->vis_lines = MIN(req->max_items, req->vis_lines); - - // Title screen passport is at a different pitch. - if (g_InvMode == INV_TITLE_MODE) { - req->y = (-Screen_GetResHeightDownscaled(RSR_TEXT) / 2) - + (req->line_height * req->vis_lines); - } else { - req->y = (-Screen_GetResHeightDownscaled(RSR_TEXT) / 1.73) - + (req->line_height * req->vis_lines); + if (save_slot == -1) { + save_slot = Savegame_GetMostRecentlyCreatedSlot(); } - Savegame_FillAvailableSaves(req); -} - -static void M_RestoreSaveRequester(void) -{ - CLAMP(g_SavegameRequester.requested, 0, g_SavegameRequester.items_used - 1); + const UI_SAVE_SLOT_DIALOG_TYPE dialog_type = page_num == PAGE_1 + ? UI_SAVE_SLOT_DIALOG_LOAD_GAME + : UI_SAVE_SLOT_DIALOG_SAVE_GAME; + m_State.save_slot.state = UI_SaveSlotDialog_Init(dialog_type, save_slot); } static void M_InitSelectLevelRequester(void) { - REQUEST_INFO *req = &m_SelectLevelRequester; - req->is_blockable = true; - Requester_ClearTextstrings(req); - Requester_SetHeading(req, GS(PASSPORT_SELECT_LEVEL)); - - if (Screen_GetResHeightDownscaled(RSR_TEXT) <= 240) { - req->vis_lines = 5; - } else if (Screen_GetResHeightDownscaled(RSR_TEXT) <= 384) { - req->vis_lines = 7; - } else if (Screen_GetResHeightDownscaled(RSR_TEXT) <= 480) { - req->vis_lines = 10; - } else { - req->vis_lines = 12; - } - - // Title screen passport is at a different pitch. - if (g_InvMode == INV_TITLE_MODE) { - req->y = (-Screen_GetResHeightDownscaled(RSR_TEXT) / 2) - + (req->line_height * req->vis_lines); - } else { - req->y = (-Screen_GetResHeightDownscaled(RSR_TEXT) / 1.73) - + (req->line_height * req->vis_lines); - } - - Savegame_FillAvailableLevels(req); + m_State.select_level.state = + UI_SelectLevelDialog_Init(g_GameInfo.select_save_slot); } -static void M_InitNewGameRequester(void) +static void M_ShowSaves(const PASSPORT_MODE pending_mode) { - REQUEST_INFO *req = &m_NewGameRequester; - Requester_ClearTextstrings(req); - Requester_SetHeading(req, GS(PASSPORT_SELECT_MODE)); - Requester_AddItem(req, false, "%s", GS(PASSPORT_MODE_NEW_GAME)); - Requester_AddItem(req, false, "%s", GS(PASSPORT_MODE_NEW_GAME_PLUS)); - Requester_AddItem(req, false, "%s", GS(PASSPORT_MODE_NEW_GAME_JP)); - Requester_AddItem(req, false, "%s", GS(PASSPORT_MODE_NEW_GAME_JP_PLUS)); - req->vis_lines = MAX_GAME_MODES; - - req->line_offset = 0; - req->requested = 0; - - // Title screen passport is at a different pitch. - if (g_InvMode == INV_TITLE_MODE) { - req->y = (-Screen_GetResHeightDownscaled(RSR_TEXT) / 2.4) - + (req->line_height * req->vis_lines + 1); - } else { - req->y = (-Screen_GetResHeightDownscaled(RSR_TEXT) / 2) - + (req->line_height * req->vis_lines); - } -} - -static void M_ShowSaves(PASSPORT_MODE pending_mode) -{ - int32_t select = Requester_Display(&g_SavegameRequester); - if (select == 0) { + const UI_SAVE_SLOT_DIALOG_CHOICE choice = + UI_SaveSlotDialog_Control(m_State.save_slot.state); + switch (choice.action) { + case UI_SAVE_SLOT_DIALOG_NO_CHOICE: g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; - } else if (select > 0) { + break; + + case UI_SAVE_SLOT_DIALOG_CANCEL: + if (g_Inv_Mode != INV_SAVE_MODE && g_Inv_Mode != INV_SAVE_CRYSTAL_MODE + && g_Inv_Mode != INV_LOAD_MODE) { + m_State.mode = PASSPORT_MODE_BROWSE; + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + } else { + m_State.mode = PASSPORT_MODE_BROWSE; + } + break; + + case UI_SAVE_SLOT_DIALOG_DETAILS: + g_GameInfo.select_save_slot = choice.slot_num; + M_InitSelectLevelRequester(); + m_State.mode = PASSPORT_MODE_SELECT_LEVEL; + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + M_ShowSelectLevel(); + break; + + case UI_SAVE_SLOT_DIALOG_CONFIRM: m_State.mode = PASSPORT_MODE_BROWSE; - g_GameInfo.select_save_slot = select - 1; + g_GameInfo.select_save_slot = choice.slot_num; g_GameInfo.passport_selection = pending_mode; - } else if ( - g_InvMode != INV_SAVE_MODE && g_InvMode != INV_SAVE_CRYSTAL_MODE - && g_InvMode != INV_LOAD_MODE) { - m_State.mode = PASSPORT_MODE_BROWSE; - g_Input = (INPUT_STATE) {}; - g_InputDB = (INPUT_STATE) {}; - } else { - m_State.mode = PASSPORT_MODE_BROWSE; + break; } } static void M_ShowSelectLevel(void) { - int32_t select = Requester_Display(&m_SelectLevelRequester); - if (select) { - if (select - 1 + GF_GetFirstLevel()->num - == Savegame_GetLevelNumber(g_GameInfo.select_save_slot) + 1) { - g_GameInfo.passport_selection = PASSPORT_MODE_STORY_SO_FAR; - } else if (select > 0) { - g_GameInfo.select_level_num = select - 1 + GF_GetFirstLevel()->num; - g_GameInfo.passport_selection = PASSPORT_MODE_SELECT_LEVEL; - Savegame_BindSlot(g_GameInfo.select_save_slot); - } else if ( - g_InvMode != INV_SAVE_MODE && g_InvMode != INV_SAVE_CRYSTAL_MODE - && g_InvMode != INV_LOAD_MODE) { - g_Input = (INPUT_STATE) {}; - g_InputDB = (INPUT_STATE) {}; - } - m_State.mode = PASSPORT_MODE_BROWSE; + const int32_t choice = + UI_SelectLevelDialog_Control(m_State.select_level.state); + if (choice == UI_SELECT_LEVEL_CHOICE_PLAY_STORY_SO_FAR) { + g_GameInfo.passport_selection = PASSPORT_MODE_STORY_SO_FAR; + } else if (choice != UI_SELECT_LEVEL_CHOICE_NOOP) { + g_GameInfo.select_level_num = choice + GF_GetFirstLevel()->num; + g_GameInfo.passport_selection = PASSPORT_MODE_SELECT_LEVEL; + Savegame_BindSlot(g_GameInfo.select_save_slot); } else { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; @@ -452,8 +356,6 @@ static void M_ShowSelectLevel(void) static void M_LoadGame(void) { M_ChangePageTextContent(GS(PASSPORT_LOAD_GAME)); - g_SavegameRequester.is_blockable = true; - if (m_State.mode == PASSPORT_MODE_BROWSE) { if (g_InputDB.menu_confirm) { M_InitSaveRequester(m_State.active_page); @@ -462,44 +364,7 @@ static void M_LoadGame(void) m_State.mode = PASSPORT_MODE_LOAD_GAME; } } else if (m_State.mode == PASSPORT_MODE_LOAD_GAME) { - M_RestoreSaveRequester(); - if (!g_SavegameRequester.items[g_SavegameRequester.requested].is_blocked - || !g_SavegameRequester.is_blockable) { - if (g_InputDB.menu_right) { - g_GameInfo.select_save_slot = g_SavegameRequester.requested; - Text_Hide(m_Text[TEXT_LEVEL_ARROW_RIGHT], true); - Requester_ClearTextstrings(&g_SavegameRequester); - M_InitSelectLevelRequester(); - m_State.mode = PASSPORT_MODE_SELECT_LEVEL; - g_Input = (INPUT_STATE) {}; - g_InputDB = (INPUT_STATE) {}; - M_ShowSelectLevel(); - } else { - M_ShowSaves(PASSPORT_MODE_LOAD_GAME); - if (m_State.mode == PASSPORT_MODE_LOAD_GAME) { - const REQUESTER_ITEM *const row_item = - &g_SavegameRequester.items - [g_SavegameRequester.requested - - g_SavegameRequester.line_offset]; - if (row_item->content != nullptr) { - Text_SetPos( - m_Text[TEXT_LEVEL_ARROW_RIGHT], 130, - row_item->content->pos.y); - Text_Hide(m_Text[TEXT_LEVEL_ARROW_RIGHT], false); - } - } else { - Text_Hide(m_Text[TEXT_LEVEL_ARROW_RIGHT], true); - } - } - } else { - M_ShowSaves(PASSPORT_MODE_LOAD_GAME); - Text_Hide(m_Text[TEXT_LEVEL_ARROW_RIGHT], true); - } - - if (g_SavegameRequester.items[g_SavegameRequester.requested].is_blocked - && g_SavegameRequester.is_blockable) { - Text_Hide(m_Text[TEXT_LEVEL_ARROW_RIGHT], true); - } + M_ShowSaves(PASSPORT_MODE_LOAD_GAME); } else if (m_State.mode == PASSPORT_MODE_SELECT_LEVEL) { M_SelectLevel(); } @@ -508,8 +373,6 @@ static void M_LoadGame(void) static void M_SelectLevel(void) { if (g_InputDB.menu_left) { - Text_Hide(m_Text[TEXT_LEVEL_ARROW_LEFT], true); - Requester_ClearTextstrings(&m_SelectLevelRequester); M_InitSaveRequester(m_State.active_page); m_State.mode = PASSPORT_MODE_LOAD_GAME; g_Input = (INPUT_STATE) {}; @@ -517,31 +380,12 @@ static void M_SelectLevel(void) M_ShowSaves(PASSPORT_MODE_LOAD_GAME); } else { M_ShowSelectLevel(); - if (m_State.mode == PASSPORT_MODE_SELECT_LEVEL) { - const TEXTSTRING *const sel_item = - m_SelectLevelRequester - .items - [m_SelectLevelRequester.requested - - m_SelectLevelRequester.line_offset] - .content; - if (sel_item != nullptr) { - Text_SetPos( - m_Text[TEXT_LEVEL_ARROW_LEFT], -130, sel_item->pos.y); - Text_Hide(m_Text[TEXT_LEVEL_ARROW_LEFT], false); - } else { - Text_Hide(m_Text[TEXT_LEVEL_ARROW_LEFT], true); - } - } else { - Text_Hide(m_Text[TEXT_LEVEL_ARROW_LEFT], true); - } } } static void M_SaveGame(void) { M_ChangePageTextContent(GS(PASSPORT_SAVE_GAME)); - g_SavegameRequester.is_blockable = false; - if (m_State.mode == PASSPORT_MODE_BROWSE) { if (g_InputDB.menu_confirm) { M_InitSaveRequester(m_State.active_page); @@ -550,7 +394,6 @@ static void M_SaveGame(void) m_State.mode = PASSPORT_MODE_SAVE_GAME; } } else if (m_State.mode == PASSPORT_MODE_SAVE_GAME) { - M_RestoreSaveRequester(); M_ShowSaves(PASSPORT_MODE_SAVE_GAME); } } @@ -558,54 +401,48 @@ static void M_SaveGame(void) static void M_NewGame(void) { M_ChangePageTextContent(GS(PASSPORT_NEW_GAME)); - if (m_State.mode == PASSPORT_MODE_BROWSE) { if (g_InputDB.menu_confirm && (g_Config.gameplay.enable_game_modes || g_Config.profile.new_game_plus_unlock)) { - M_InitNewGameRequester(); g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; m_State.mode = PASSPORT_MODE_NEW_GAME; + m_State.new_game.is_ready = true; } else { - g_GameInfo.save_initial_version = SAVEGAME_CURRENT_VERSION; - g_GameInfo.bonus_level_unlock = false; + Savegame_SetInitialVersion(SAVEGAME_CURRENT_VERSION); g_GameInfo.passport_selection = PASSPORT_MODE_NEW_GAME; } } else if (m_State.mode == PASSPORT_MODE_NEW_GAME) { - int32_t select = Requester_Display(&m_NewGameRequester); - if (select) { - if (select > 0) { - switch (select - 1) { - case 0: - g_GameInfo.bonus_flag = 0; - break; - case 1: - g_GameInfo.bonus_flag = GBF_NGPLUS; - break; - case 2: - g_GameInfo.bonus_flag = GBF_JAPANESE; - break; - case 3: - g_GameInfo.bonus_flag = GBF_JAPANESE | GBF_NGPLUS; - break; - default: - g_GameInfo.bonus_flag = 0; - break; - } - g_GameInfo.bonus_level_unlock = false; - g_GameInfo.passport_selection = PASSPORT_MODE_NEW_GAME; - g_GameInfo.save_initial_version = SAVEGAME_CURRENT_VERSION; - } else if ( - g_InvMode != INV_SAVE_MODE && g_InvMode != INV_SAVE_CRYSTAL_MODE - && g_InvMode != INV_LOAD_MODE) { - g_Input = (INPUT_STATE) {}; - g_InputDB = (INPUT_STATE) {}; - } - m_State.mode = PASSPORT_MODE_BROWSE; - } else { + const int32_t choice = UI_NewGame_Control(&m_State.new_game.state); + if (choice == UI_REQUESTER_NO_CHOICE) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; + } else if (choice == UI_REQUESTER_CANCEL) { + m_State.new_game.is_ready = false; + m_State.mode = PASSPORT_MODE_BROWSE; + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + } else { + switch (choice) { + case 0: + Game_SetBonusFlag(GBF_NONE); + break; + case 1: + Game_SetBonusFlag(GBF_NGPLUS); + break; + case 2: + Game_SetBonusFlag(GBF_JAPANESE); + break; + case 3: + Game_SetBonusFlag(GBF_JAPANESE | GBF_NGPLUS); + break; + default: + Game_SetBonusFlag(GBF_NONE); + break; + } + g_GameInfo.passport_selection = PASSPORT_MODE_NEW_GAME; + Savegame_SetInitialVersion(SAVEGAME_CURRENT_VERSION); } } } @@ -729,14 +566,16 @@ void Option_Passport_Control(INVENTORY_ITEM *inv_item, const bool is_busy) } else if (m_State.current_page > m_State.active_page) { M_FlipLeft(inv_item); } else { + m_State.is_ready = true; M_SyncArrowsVisibility(); M_ShowPage(inv_item); if (g_InputDB.menu_confirm) { M_Close(inv_item); m_State.active_page = -1; } else if (g_InputDB.menu_back) { - if (g_InvMode != INV_DEATH_MODE - && m_State.mode == PASSPORT_MODE_BROWSE) { + if (g_Inv_Mode != INV_DEATH_MODE + && (m_State.mode == PASSPORT_MODE_BROWSE + || m_State.mode == PASSPORT_MODE_RESTART)) { M_Close(inv_item); m_State.active_page = -1; } else { @@ -749,6 +588,33 @@ void Option_Passport_Control(INVENTORY_ITEM *inv_item, const bool is_busy) } } +void Option_Passport_Draw(INVENTORY_ITEM *const item) +{ + switch (m_State.mode) { + case PASSPORT_MODE_NEW_GAME: + if (m_State.new_game.is_ready) { + UI_NewGame(&m_State.new_game.state); + } + break; + + case PASSPORT_MODE_SELECT_LEVEL: + if (m_State.select_level.state != nullptr) { + UI_SelectLevelDialog(m_State.select_level.state); + } + break; + + case PASSPORT_MODE_LOAD_GAME: + case PASSPORT_MODE_SAVE_GAME: + if (m_State.is_ready && m_State.save_slot.state != nullptr) { + UI_SaveSlotDialog(m_State.save_slot.state); + } + break; + + default: + break; + } +} + void Option_Passport_Shutdown(void) { M_RemoveAllText(); diff --git a/src/tr1/game/option/option_passport.h b/src/tr1/game/option/option_passport.h index 8161fac1f..7f94dffbd 100644 --- a/src/tr1/game/option/option_passport.h +++ b/src/tr1/game/option/option_passport.h @@ -3,4 +3,5 @@ #include void Option_Passport_Control(INVENTORY_ITEM *inv_item, bool is_busy); +void Option_Passport_Draw(INVENTORY_ITEM *inv_item); void Option_Passport_Shutdown(void); diff --git a/src/tr1/game/output.c b/src/tr1/game/output.c index f9c27f567..9bed2681b 100644 --- a/src/tr1/game/output.c +++ b/src/tr1/game/output.c @@ -1,611 +1,504 @@ #include "game/output.h" -#include "game/clock.h" +#include "game/level.h" +#include "game/output/meshes/common.h" +#include "game/output/meshes/rooms.h" #include "game/output/sprites.h" #include "game/output/textures.h" #include "game/overlay.h" -#include "game/random.h" -#include "game/room.h" #include "game/shell.h" #include "game/viewport.h" -#include "global/const.h" -#include "global/types.h" #include "global/vars.h" -#include "specific/s_output.h" #include #include -#include #include -#include -#include -#include -#include #include -#include - -#include -#include #define MAX_LIGHTNINGS 64 -#define PHD_IONE (PHD_ONE / 4) +#define MAP_DEPTH(zv) (g_FltResZBuf - g_FltResZ * (1.0 / (double)(zv))) +#define TEXT_OUTLINE_THICKNESS 2 + +typedef enum { + MC_PURPLE_C, + MC_PURPLE_E, + MC_BROWN_C, + MC_BROWN_E, + MC_GREY_C, + MC_GREY_E, + MC_GREY_TL, + MC_GREY_TR, + MC_GREY_BL, + MC_GREY_BR, + MC_BLACK, + MC_GOLD_LIGHT, + MC_GOLD_DARK, + MC_NUMBER_OF, +} MENU_COLOR; typedef struct { - struct { - XYZ_32 pos; - int32_t thickness; - } edges[2]; + int32_t x; + int32_t y; + int32_t w; + int32_t h; +} QUAD_INFO; + +typedef struct { + XYZ_32 pos_0; + XYZ_32 pos_1; + int32_t thickness; } LIGHTNING; -typedef struct { - int16_t poly_count; - int16_t vertex_count; - XYZ_16 vertices[32]; -} SHADOW_INFO; - -static int32_t m_LsAdder = 0; -static int32_t m_LsDivider = 0; -static bool m_IsSkyboxEnabled = false; -static bool m_IsWibbleEffect = false; -static bool m_IsWaterEffect = false; -static bool m_IsShadeEffect = false; -static int m_OverlayCurAlpha = 0; -static int m_OverlayDstAlpha = 0; -static int m_BackdropCurAlpha = 0; -static int m_BackdropDstAlpha = 0; - -static int32_t m_WibbleOffset = 0; -static int32_t m_AnimatedTexturesOffset = 0; -static CLOCK_TIMER m_WibbleTimer = { .type = CLOCK_TIMER_SIM }; -static CLOCK_TIMER m_AnimatedTexturesTimer = { .type = CLOCK_TIMER_SIM }; -static CLOCK_TIMER m_FadeTimer = { .type = CLOCK_TIMER_SIM }; -static int32_t m_WibbleTable[WIBBLE_SIZE] = {}; -static int32_t m_ShadeTable[WIBBLE_SIZE] = {}; -static int32_t m_RandTable[WIBBLE_SIZE] = {}; - -static PHD_VBUF *m_VBuf = nullptr; -static TEXTURE_UV *m_EnvMapUV = nullptr; -static int32_t m_DrawDistFade = 0; -static int32_t m_DrawDistMax = 0; -static RGB_F m_WaterColor = {}; -static XYZ_32 m_LsVectorView = {}; - static int32_t m_LightningCount = 0; static LIGHTNING m_LightningTable[MAX_LIGHTNINGS]; +static int32_t m_TextureMap[GFX_MAX_TEXTURES] = { GFX_NO_TEXTURE }; + +static GFX_2D_RENDERER *m_Renderer2D = nullptr; +static GFX_3D_RENDERER *m_Renderer3D = nullptr; +static bool m_IsTextureMode = false; +static int32_t m_SelectedTexture = -1; + +static int32_t m_SurfaceWidth = 0; +static int32_t m_SurfaceHeight = 0; +static GFX_2D_SURFACE *m_PictureSurface = nullptr; +static GFX_2D_SURFACE *m_TextureSurfaces[GFX_MAX_TEXTURES] = { nullptr }; -static char *m_BackdropImagePath = nullptr; static const char *m_ImageExtensions[] = { ".png", ".jpg", ".jpeg", ".pcx", nullptr, }; -static struct { - GLint bound_program; - GLint bound_vao; - GLint bound_vbo; - GLint bound_texture; -} m_CachedState; +static RGBA_8888 m_MenuColorMap[MC_NUMBER_OF] = { + { 70, 30, 107, 230 }, // MC_PURPLE_C + { 70, 30, 107, 0 }, // MC_PURPLE_E + { 91, 46, 9, 255 }, // MC_BROWN_C + { 91, 46, 9, 0 }, // MC_BROWN_E + { 197, 197, 197, 255 }, // MC_GREY_C + { 45, 45, 45, 255 }, // MC_GREY_E + { 96, 96, 96, 255 }, // MC_GREY_TL + { 32, 32, 32, 255 }, // MC_GREY_TR + { 63, 63, 63, 255 }, // MC_GREY_BL + { 0, 0, 0, 255 }, // MC_GREY_BR + { 0, 0, 0, 255 }, // MC_BLACK + { 232, 192, 112, 255 }, // MC_GOLD_LIGHT + { 140, 112, 56, 255 }, // MC_GOLD_DARK +}; -static void M_DrawSphere(const XYZ_32 pos, const int32_t radius); -static void M_DrawFlatFace3s(const FACE3 *faces, int32_t count); -static void M_DrawFlatFace4s(const FACE4 *faces, int32_t count); -static void M_DrawTexturedFace3s(const FACE3 *faces, int32_t count); -static void M_DrawTexturedFace4s(const FACE4 *faces, int32_t count); -static void M_DrawObjectFace3EnvMap(const FACE3 *faces, int32_t count); -static void M_DrawObjectFace4EnvMap(const FACE4 *faces, int32_t count); -static uint16_t M_CalcVertex(PHD_VBUF *vbuf, const XYZ_16 pos); -static void M_CalcVertexWibble(PHD_VBUF *vbuf); -static bool M_CalcObjectVertices(const XYZ_16 *vertices, int16_t count); -static void M_CalcVerticeLight(const OBJECT_MESH *mesh); -static bool M_CalcVerticeEnvMap(const OBJECT_MESH *mesh); -static void M_CalcSkyboxLight(const OBJECT_MESH *mesh); -static void M_CalcRoomVertices(const ROOM_MESH *mesh); -static void M_CalcRoomVerticesWibble(const ROOM_MESH *mesh); -static void M_CalcWibbleTable(void); +static RGBA_8888 M_GetMenuColor(MENU_COLOR color); +static void M_SelectTexture(int32_t texture_num); +static void M_EnableDepthWrites(void); +static void M_DisableDepthWrites(void); +static void M_EnableDepthTest(void); +static void M_DisableDepthTest(void); +static void M_EnableTextureMode(void); +static void M_DisableTextureMode(void); +static void M_DownloadBackdropSurface(const IMAGE *image); +static void M_DownloadTextures(int32_t pages); +static void M_ReleaseTextures(void); +static void M_ReleaseSurfaces(void); +static void M_Flush(void); +static void M_FlipScreen(void); -static void M_DrawSphere(const XYZ_32 pos, const int32_t radius) +static void M_DrawTriangleFan( + const GFX_3D_VERTEX *vertices, int32_t vertex_count); +static void M_DrawTriangleStrip( + const GFX_3D_VERTEX *vertices, int32_t vertex_count); +static void M_Draw2DQuad( + int32_t x1, int32_t y1, int32_t x2, int32_t y2, RGBA_8888 tl, RGBA_8888 tr, + RGBA_8888 bl, RGBA_8888 br); +static void M_DrawLightningSegment(const LIGHTNING *const lightning); +static void M_DrawSprite( + int16_t x1, int16_t y1, int16_t x2, int32_t y2, int32_t z, + int32_t sprite_idx, int16_t shade); + +static RGBA_8888 M_GetMenuColor(MENU_COLOR color) { - bool wireframe_state = GFX_Context_GetWireframeMode(); - GFX_Context_SetWireframeMode(true); + return m_MenuColorMap[color]; +} - RGBA_8888 color = { .r = 255, .g = 255, .b = 255, .a = 128 }; - if (wireframe_state) { - color = (RGBA_8888) { .r = 0, .g = 0, .b = 0, .a = 128 }; +static void M_SelectTexture(const int32_t texture_num) +{ + if (texture_num == m_SelectedTexture) { + return; } + if (m_TextureMap[texture_num] == GFX_NO_TEXTURE) { + LOG_ERROR("ERROR: Attempt to select unloaded texture"); + return; + } + GFX_3D_Renderer_SelectTexture(m_Renderer3D, m_TextureMap[texture_num]); + m_SelectedTexture = texture_num; +} - S_Output_DisableTextureMode(); - S_Output_SetBlendingMode(GFX_BLEND_MODE_NORMAL); +static void M_EnableDepthWrites(void) +{ + GFX_3D_Renderer_SetDepthWritesEnabled(m_Renderer3D, true); +} - // More subdivisions means smoother spheres. - const int32_t subdivisions = 12; - PHD_VBUF vertices[(subdivisions + 1) * (subdivisions + 1)]; - int32_t index = 0; +static void M_DisableDepthWrites(void) +{ + GFX_3D_Renderer_SetDepthWritesEnabled(m_Renderer3D, false); +} - for (int32_t i = 0; i <= subdivisions; i++) { - const float theta = (M_PI * i) / subdivisions; // Latitude angle - const float sin_theta = sinf(theta); - const float cos_theta = cosf(theta); +static void M_EnableDepthTest(void) +{ + GFX_3D_Renderer_SetDepthTestEnabled(m_Renderer3D, true); +} - for (int32_t j = 0; j <= subdivisions; j++) { - const float phi = (2 * M_PI * j) / subdivisions; // Longitude angle - const float sin_phi = sinf(phi); - const float cos_phi = cosf(phi); +static void M_DisableDepthTest(void) +{ + GFX_3D_Renderer_SetDepthTestEnabled(m_Renderer3D, false); +} - // Convert spherical coordinates to 3D points. - XYZ_16 vertex_pos = { - .x = pos.x + radius * cos_phi * sin_theta, - .y = pos.y + radius * cos_theta, - .z = pos.z + radius * sin_phi * sin_theta, +static void M_EnableTextureMode(void) +{ + if (!m_IsTextureMode) { + m_IsTextureMode = true; + GFX_3D_Renderer_SetTexturingEnabled(m_Renderer3D, m_IsTextureMode); + } +} + +static void M_DisableTextureMode(void) +{ + if (m_IsTextureMode) { + m_IsTextureMode = false; + GFX_3D_Renderer_SetTexturingEnabled(m_Renderer3D, m_IsTextureMode); + } +} + +static void M_DownloadBackdropSurface(const IMAGE *const image) +{ + GFX_2D_Surface_Free(m_PictureSurface); + m_PictureSurface = nullptr; + if (image == nullptr) { + return; + } + m_PictureSurface = GFX_2D_Surface_CreateFromImage(image); + GFX_2D_Renderer_Upload( + m_Renderer2D, &m_PictureSurface->desc, m_PictureSurface->buffer); +} + +static void M_DownloadTextures(const int32_t pages) +{ + if (pages > GFX_MAX_TEXTURES) { + Shell_ExitSystem("Attempt to download more than texture page limit"); + } + M_ReleaseTextures(); + + for (int32_t i = 0; i < pages; i++) { + if (m_TextureSurfaces[i] == nullptr) { + const GFX_2D_SURFACE_DESC surface_desc = { + .width = TEXTURE_PAGE_WIDTH, + .height = TEXTURE_PAGE_HEIGHT, }; - - M_CalcVertex(&vertices[index], vertex_pos); - vertices[index].g = HIGH_LIGHT; - index++; + m_TextureSurfaces[i] = GFX_2D_Surface_Create(&surface_desc); } - } - - for (int32_t i = 0; i < subdivisions; i++) { - for (int32_t j = 0; j < subdivisions; j++) { - const int32_t index_0 = i * (subdivisions + 1) + j; - const int32_t index_1 = (i + 1) * (subdivisions + 1) + j; - const int32_t index_2 = (i + 1) * (subdivisions + 1) + (j + 1); - const int32_t index_3 = i * (subdivisions + 1) + (j + 1); - S_Output_DrawFlatTriangle( - &vertices[index_0], &vertices[index_1], &vertices[index_2], - color); - S_Output_DrawFlatTriangle( - &vertices[index_0], &vertices[index_2], &vertices[index_3], - color); - } - } - - S_Output_SetBlendingMode(GFX_BLEND_MODE_OFF); - GFX_Context_SetWireframeMode(wireframe_state); -} - -static void M_DrawFlatFace3s(const FACE3 *const faces, const int32_t count) -{ - S_Output_DisableTextureMode(); - - for (int32_t i = 0; i < count; i++) { - const FACE3 *const face = &faces[i]; - PHD_VBUF *const vns[3] = { - &m_VBuf[face->vertices[0]], - &m_VBuf[face->vertices[1]], - &m_VBuf[face->vertices[2]], - }; - - const RGBA_8888 color = - Output_RGB2RGBA(Output_GetPaletteColor8(face->palette_idx)); - S_Output_DrawFlatTriangle(vns[0], vns[1], vns[2], color); + GFX_2D_SURFACE *const surface = m_TextureSurfaces[i]; + RGBA_8888 *const output_ptr = (RGBA_8888 *)surface->buffer; + const RGBA_8888 *const input_ptr = Output_GetTexturePage32(i); + memcpy( + output_ptr, input_ptr, + surface->desc.width * surface->desc.height * sizeof(RGBA_8888)); + m_TextureMap[i] = GFX_3D_Renderer_RegisterTexturePage( + m_Renderer3D, output_ptr, surface->desc.width, + surface->desc.height); } } -static void M_DrawFlatFace4s(const FACE4 *const faces, const int32_t count) +static void M_ReleaseTextures(void) { - S_Output_DisableTextureMode(); + if (m_Renderer3D == nullptr) { + return; + } + for (int32_t i = 0; i < GFX_MAX_TEXTURES; i++) { + if (m_TextureMap[i] != GFX_NO_TEXTURE) { + GFX_3D_Renderer_UnregisterTexturePage( + m_Renderer3D, m_TextureMap[i]); + m_TextureMap[i] = GFX_NO_TEXTURE; + } + } + m_SelectedTexture = -1; +} - for (int32_t i = 0; i < count; i++) { - const FACE4 *const face = &faces[i]; - PHD_VBUF *const vns[4] = { - &m_VBuf[face->vertices[0]], - &m_VBuf[face->vertices[1]], - &m_VBuf[face->vertices[2]], - &m_VBuf[face->vertices[3]], - }; - - const RGBA_8888 color = - Output_RGB2RGBA(Output_GetPaletteColor8(face->palette_idx)); - S_Output_DrawFlatTriangle(vns[0], vns[1], vns[2], color); - S_Output_DrawFlatTriangle(vns[2], vns[3], vns[0], color); +static void M_ReleaseSurfaces(void) +{ + for (int32_t i = 0; i < GFX_MAX_TEXTURES; i++) { + if (m_TextureSurfaces[i] != nullptr) { + GFX_2D_Surface_Free(m_TextureSurfaces[i]); + m_TextureSurfaces[i] = nullptr; + } + } + if (m_PictureSurface != nullptr) { + GFX_2D_Surface_Free(m_PictureSurface); + m_PictureSurface = nullptr; } } -static void M_DrawTexturedFace3s(const FACE3 *const faces, const int32_t count) +static void M_Flush(void) { - S_Output_EnableTextureMode(); - - for (int32_t i = 0; i < count; i++) { - const FACE3 *const face = &faces[i]; - PHD_VBUF *const vns[3] = { - &m_VBuf[face->vertices[0]], - &m_VBuf[face->vertices[1]], - &m_VBuf[face->vertices[2]], - }; - - OBJECT_TEXTURE *const tex = Output_GetObjectTexture(face->texture_idx); - for (int32_t j = 0; j < 3; j++) { - vns[j]->u = tex->uv[j].u; - vns[j]->v = tex->uv[j].v; - vns[j]->z = 1.0f; - vns[j]->w = 1.0f; - } - - S_Output_DrawTexturedTriangle( - vns[0], vns[1], vns[2], tex->tex_page, tex->draw_type); - } + GFX_3D_Renderer_Flush(m_Renderer3D); } -static void M_DrawTexturedFace4s(const FACE4 *const faces, const int32_t count) +static void M_FlipScreen(void) { - S_Output_EnableTextureMode(); - - for (int32_t i = 0; i < count; i++) { - const FACE4 *const face = &faces[i]; - PHD_VBUF *const vns[4] = { - &m_VBuf[face->vertices[0]], - &m_VBuf[face->vertices[1]], - &m_VBuf[face->vertices[2]], - &m_VBuf[face->vertices[3]], - }; - - OBJECT_TEXTURE *const tex = Output_GetObjectTexture(face->texture_idx); - for (int32_t j = 0; j < 4; j++) { - vns[j]->u = tex->uv[j].u; - vns[j]->v = tex->uv[j].v; - if (g_Config.rendering.enable_trapezoid_filter) { - vns[j]->z = face->texture_zw[j].z; - vns[j]->w = face->texture_zw[j].w; - } else { - vns[j]->z = 1.0f; - vns[j]->w = 1.0f; - } - } - - S_Output_DrawTexturedQuad( - vns[0], vns[1], vns[2], vns[3], tex->tex_page, tex->draw_type); - } + GFX_Context_SwapBuffers(); + m_SelectedTexture = -1; } -static void M_DrawObjectFace3EnvMap( - const FACE3 *const faces, const int32_t count) +static void M_DrawBackdropSurface(void) { - for (int32_t i = 0; i < count; i++) { - const FACE3 *const face = &faces[i]; - PHD_VBUF *vns[3] = { - &m_VBuf[face->vertices[0]], - &m_VBuf[face->vertices[1]], - &m_VBuf[face->vertices[2]], - }; - - for (int32_t j = 0; j < 3; j++) { - vns[j]->u = m_EnvMapUV[face->vertices[j]].u; - vns[j]->v = m_EnvMapUV[face->vertices[j]].v; - } - - if (face->enable_reflections) { - S_Output_DrawEnvMapTriangle(vns[0], vns[1], vns[2]); - } - } } -static void M_DrawObjectFace4EnvMap( - const FACE4 *const faces, const int32_t count) +static void M_DrawTriangleFan( + const GFX_3D_VERTEX *const vertices, const int32_t vertex_count) { - for (int32_t i = 0; i < count; i++) { - const FACE4 *const face = &faces[i]; - PHD_VBUF *vns[4] = { - &m_VBuf[face->vertices[0]], - &m_VBuf[face->vertices[1]], - &m_VBuf[face->vertices[2]], - &m_VBuf[face->vertices[3]], - }; - - for (int32_t j = 0; j < 4; j++) { - vns[j]->u = m_EnvMapUV[face->vertices[j]].u; - vns[j]->v = m_EnvMapUV[face->vertices[j]].v; - } - - if (face->enable_reflections) { - S_Output_DrawEnvMapQuad(vns[0], vns[1], vns[2], vns[3]); - } - } + GFX_3D_Renderer_RenderPrimFan(m_Renderer3D, vertices, vertex_count); } -static uint16_t M_CalcVertex(PHD_VBUF *const vbuf, const XYZ_16 pos) +static void M_DrawTriangleStrip( + const GFX_3D_VERTEX *const vertices, const int32_t vertex_count) { + GFX_3D_Renderer_RenderPrimStrip(m_Renderer3D, vertices, vertex_count); +} + +static void M_Draw2DQuad( + const int32_t x1, const int32_t y1, const int32_t x2, const int32_t y2, + const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, + const RGBA_8888 br) +{ + int32_t vertex_count = 4; + GFX_3D_VERTEX vertices[vertex_count]; + +#define SET(vtx_idx, x_, y_, z_, color) \ + vertices[vtx_idx].x = x_, vertices[vtx_idx].y = y_, \ + vertices[vtx_idx].z = 1.0f, vertices[vtx_idx].r = color.r; \ + vertices[vtx_idx].g = color.g; \ + vertices[vtx_idx].b = color.b; \ + vertices[vtx_idx].a = color.a; + SET(0, x1, y1, 1.0f, tl); + SET(1, x2, y1, 1.0f, tr); + SET(2, x2, y2, 1.0f, br); + SET(3, x1, y2, 1.0f, bl); +#undef SET + + M_DisableTextureMode(); + GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_NORMAL); + M_DrawTriangleFan(vertices, vertex_count); + GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); +} + +static void M_DrawLightningSegment(const LIGHTNING *const lightning) +{ + const int32_t vertex_count = 8; + const RGB_F blue = { 0.0f, 0.0f, 255.0f }; + const RGB_F white = { 255.0f, 255.0f, 255.0f }; + GFX_3D_VERTEX vertices[vertex_count]; + + const XYZ_32 wp0 = lightning->pos_0; + const XYZ_32 wp1 = lightning->pos_1; + if (wp0.z < Output_GetNearZ() || wp0.z > Output_GetFarZ() + || wp1.z < Output_GetNearZ() || wp1.z > Output_GetFarZ()) { + return; + } + + const int32_t vcx = Viewport_GetCenterX(); + const int32_t vcy = Viewport_GetCenterY(); + const float zp0 = wp0.z / g_PhdPersp; + const float zp1 = wp1.z / g_PhdPersp; + const XYZ_32 p0 = { vcx + wp0.x / zp0, vcy + wp0.y / zp0, wp0.z }; + const XYZ_32 p1 = { vcx + wp1.x / zp1, vcy + wp1.y / zp1, wp1.z }; + const int32_t t1 = (lightning->thickness << W2V_SHIFT) / zp0; + const int32_t t2 = (lightning->thickness << W2V_SHIFT) / zp1; + +#define SET(vtx_idx, x_, y_, z_, color) \ + vertices[vtx_idx].x = x_; \ + vertices[vtx_idx].y = y_; \ + vertices[vtx_idx].z = MAP_DEPTH(z_); \ + vertices[vtx_idx].r = color.r; \ + vertices[vtx_idx].g = color.g; \ + vertices[vtx_idx].b = color.b; \ + vertices[vtx_idx].a = 128.0f; // clang-format off - double xv = - g_MatrixPtr->_00 * pos.x + - g_MatrixPtr->_01 * pos.y + - g_MatrixPtr->_02 * pos.z + - g_MatrixPtr->_03; - double yv = - g_MatrixPtr->_10 * pos.x + - g_MatrixPtr->_11 * pos.y + - g_MatrixPtr->_12 * pos.z + - g_MatrixPtr->_13; - double zv = - g_MatrixPtr->_20 * pos.x + - g_MatrixPtr->_21 * pos.y + - g_MatrixPtr->_22 * pos.z + - g_MatrixPtr->_23; + SET(0, p0.x, p0.y, p0.z, blue); + SET(1, p0.x + t1 / 2, p0.y, p0.z, white); + SET(2, p1.x + t2 / 2, p1.y, p1.z, white); + SET(3, p1.x, p1.y, p1.z, blue); + SET(4, p0.x + t1 / 2, p0.y, p0.z, white); + SET(5, p0.x + t1, p0.y, p0.z, blue); + SET(6, p1.x + t2, p1.y, p1.z, blue); + SET(7, p1.x + t2 / 2, p1.y, p1.z, white); // clang-format on +#undef SET - vbuf->xv = xv; - vbuf->yv = yv; - vbuf->zv = zv; + M_DisableTextureMode(); + GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_NORMAL); + M_DrawTriangleFan(&vertices[0], 4); + M_DrawTriangleFan(&vertices[4], 4); + GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); +} - uint16_t clip_flags; - if (zv < Output_GetNearZ()) { - clip_flags = 0x8000; +static void M_DrawSprite( + const int16_t x1, const int16_t y1, const int16_t x2, const int32_t y2, + const int32_t z, const int32_t sprite_idx, const int16_t shade) +{ + const int32_t vertex_count = 4; + GFX_3D_VERTEX vertices[vertex_count]; + + const float multiplier = g_Config.visuals.brightness / 16.0f; + const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_idx); + float vshade = (SHADE_MAX - shade) * multiplier; + CLAMPG(vshade, 255.0f); + + const float u0 = (sprite->offset & 0xFF) / 256.0f; + const float v0 = (sprite->offset >> 8) / 256.0f; + const float u1 = (sprite->width >> 8) / 256.0f + u0; + const float v1 = (sprite->height >> 8) / 256.0f + v0; + const float vz = MAP_DEPTH(z); + const float rhw = 1.0f / z; + +#define SET(vtx_idx, x_, y_, z_, u_, v_) \ + vertices[vtx_idx].x = x_; \ + vertices[vtx_idx].y = y_; \ + vertices[vtx_idx].z = z_; \ + vertices[vtx_idx].s = u_; \ + vertices[vtx_idx].t = v_; \ + vertices[vtx_idx].tex_coord[2] = 1.0; \ + vertices[vtx_idx].tex_coord[3] = 1.0; \ + vertices[vtx_idx].w = rhw; \ + vertices[vtx_idx].r = vshade; \ + vertices[vtx_idx].g = vshade; \ + vertices[vtx_idx].b = vshade; + + SET(0, x1, y1, vz, u0, v0); + SET(1, x2, y1, vz, u1, v0); + SET(2, x2, y2, vz, u1, v1); + SET(3, x1, y2, vz, u0, v1); +#undef SET + + if (m_TextureMap[sprite->tex_page] != GFX_NO_TEXTURE) { + M_EnableTextureMode(); + M_SelectTexture(sprite->tex_page); + M_DrawTriangleFan(vertices, vertex_count); } else { - clip_flags = 0; - - double persp = g_PhdPersp / zv; - double xs = Viewport_GetCenterX() + xv * persp; - double ys = Viewport_GetCenterY() + yv * persp; - - if (xs < g_PhdLeft) { - clip_flags |= 1; - } else if (xs > g_PhdRight) { - clip_flags |= 2; - } - - if (ys < g_PhdTop) { - clip_flags |= 4; - } else if (ys > g_PhdBottom) { - clip_flags |= 8; - } - - vbuf->xs = xs; - vbuf->ys = ys; - } - vbuf->clip = clip_flags; - return clip_flags; -} - -static void M_CalcVertexWibble(PHD_VBUF *const vbuf) -{ - double xs = vbuf->xs; - double ys = vbuf->ys; - xs += m_WibbleTable[(m_WibbleOffset + (int)ys) & (WIBBLE_SIZE - 1)]; - ys += m_WibbleTable[(m_WibbleOffset + (int)xs) & (WIBBLE_SIZE - 1)]; - - int16_t clip_flags = vbuf->clip & ~15; - if (xs < g_PhdLeft) { - clip_flags |= 1; - } else if (xs > g_PhdRight) { - clip_flags |= 2; - } - - if (ys < g_PhdTop) { - clip_flags |= 4; - } else if (ys > g_PhdBottom) { - clip_flags |= 8; - } - - vbuf->xs = xs; - vbuf->ys = ys; - vbuf->clip = clip_flags; -} - -static bool M_CalcObjectVertices( - const XYZ_16 *const vertices, const int16_t count) -{ - uint16_t total_clip = 0xFFFF; - for (int i = 0; i < count; i++) { - total_clip &= M_CalcVertex(&m_VBuf[i], vertices[i]); - } - - return total_clip == 0; -} - -static void M_CalcVerticeLight(const OBJECT_MESH *const mesh) -{ - if (mesh->num_lights <= 0) { - for (int32_t i = 0; i < -mesh->num_lights; i++) { - int16_t shade = m_LsAdder + mesh->lighting.lights[i]; - CLAMP(shade, 0, 0x1FFF); - m_VBuf[i].g = shade; - } - - return; - } - - if (m_LsDivider == 0) { - int16_t shade = m_LsAdder; - CLAMP(shade, 0, 0x1FFF); - for (int32_t i = 0; i < mesh->num_lights; i++) { - m_VBuf[i].g = shade; - } - - return; - } - - // clang-format off - const int32_t xv = ( - g_MatrixPtr->_00 * m_LsVectorView.x + - g_MatrixPtr->_10 * m_LsVectorView.y + - g_MatrixPtr->_20 * m_LsVectorView.z - ) / m_LsDivider; - - const int32_t yv = ( - g_MatrixPtr->_01 * m_LsVectorView.x + - g_MatrixPtr->_11 * m_LsVectorView.y + - g_MatrixPtr->_21 * m_LsVectorView.z - ) / m_LsDivider; - - const int32_t zv = ( - g_MatrixPtr->_02 * m_LsVectorView.x + - g_MatrixPtr->_12 * m_LsVectorView.y + - g_MatrixPtr->_22 * m_LsVectorView.z - ) / m_LsDivider; - // clang-format on - - for (int32_t i = 0; i < mesh->num_lights; i++) { - const XYZ_16 *const normal = &mesh->lighting.normals[i]; - int16_t shade = m_LsAdder - + ((normal->x * xv + normal->y * yv + normal->z * zv) >> 16); - CLAMP(shade, 0, 0x1FFF); - m_VBuf[i].g = shade; - } -} - -static bool M_CalcVerticeEnvMap(const OBJECT_MESH *mesh) -{ - if (mesh->num_lights <= 0) { - return false; - } - - for (int32_t i = 0; i < mesh->num_lights; ++i) { - // make sure that reflection will be drawn after normal poly - m_VBuf[i].zv -= (double)((1 << W2V_SHIFT) / 2); - - // set lighting that depends only from fog distance - m_VBuf[i].g = 0x1000; - - const int32_t depth = g_MatrixPtr->_23 >> W2V_SHIFT; - m_VBuf[i].g += Output_CalcFogShade(depth); - - // reflection can be darker but not brighter - CLAMP(m_VBuf[i].g, 0x1000, 0x1FFF); - - // rotate normal vectors for X/Y, no translation - const XYZ_16 *const normal = &mesh->lighting.normals[i]; - - // clang-format off - int32_t x = ( - g_MatrixPtr->_00 * normal->x + - g_MatrixPtr->_01 * normal->y + - g_MatrixPtr->_02 * normal->z - ) >> W2V_SHIFT; - - int32_t y = ( - g_MatrixPtr->_10 * normal->x + - g_MatrixPtr->_11 * normal->y + - g_MatrixPtr->_12 * normal->z - ) >> W2V_SHIFT; - // clang-format on - - CLAMP(x, -PHD_ONE, PHD_IONE); - CLAMP(y, -PHD_ONE, PHD_IONE); - m_EnvMapUV[i].u = PHD_ONE / PHD_IONE * (x + PHD_IONE) / 2; - m_EnvMapUV[i].v = PHD_ONE - PHD_ONE / PHD_IONE * (y + PHD_IONE) / 2; - } - - return true; -} - -static void M_CalcSkyboxLight(const OBJECT_MESH *const mesh) -{ - for (int32_t i = 0; i < ABS(mesh->num_lights); i++) { - m_VBuf[i].g = 0xFFF; - } -} - -static void M_CalcRoomVertices(const ROOM_MESH *const mesh) -{ - for (int32_t i = 0; i < mesh->num_vertices; i++) { - PHD_VBUF *const vbuf = &m_VBuf[i]; - const ROOM_VERTEX *const vertex = &mesh->vertices[i]; - - M_CalcVertex(vbuf, vertex->pos); - - vbuf->g = vertex->light_adder; - if (vbuf->zv >= Output_GetNearZ()) { - const int32_t depth = ((int32_t)vbuf->zv) >> W2V_SHIFT; - if (depth > Output_GetDrawDistMax()) { - vbuf->g = MAX_LIGHTING; - if (!m_IsSkyboxEnabled) { - vbuf->clip |= 16; - } - } else { - vbuf->g += Output_CalcFogShade(depth); - if (!m_IsWaterEffect) { - CLAMPG(vbuf->g, MAX_LIGHTING); - } - } - } - - if (m_IsWaterEffect) { - vbuf->g += m_ShadeTable[( - ((uint8_t)m_WibbleOffset - + (uint8_t)m_RandTable[(mesh->num_vertices - i) % WIBBLE_SIZE]) - % WIBBLE_SIZE)]; - CLAMP(vbuf->g, 0, 0x1FFF); - } - } -} - -static void M_CalcRoomVerticesWibble(const ROOM_MESH *const mesh) -{ - for (int32_t i = 0; i < mesh->num_vertices; i++) { - if (mesh->vertices[i].flags & NO_VERT_MOVE) { - continue; - } - M_CalcVertexWibble(&m_VBuf[i]); - } -} - -static void M_CalcWibbleTable(void) -{ - for (int i = 0; i < WIBBLE_SIZE; i++) { - PHD_ANGLE angle = (i * DEG_360) / WIBBLE_SIZE; - m_WibbleTable[i] = Math_Sin(angle) * MAX_WIBBLE >> W2V_SHIFT; - m_ShadeTable[i] = Math_Sin(angle) * MAX_SHADE >> W2V_SHIFT; - m_RandTable[i] = (Random_GetDraw() >> 5) - 0x01FF; + M_DisableTextureMode(); + M_DrawTriangleFan(vertices, vertex_count); } } bool Output_Init(void) { - M_CalcWibbleTable(); - Output_Sprites_Init(); + for (int32_t i = 0; i < GFX_MAX_TEXTURES; i++) { + m_TextureMap[i] = GFX_NO_TEXTURE; + m_TextureSurfaces[i] = nullptr; + } + + m_Renderer2D = GFX_2D_Renderer_Create(); + m_Renderer3D = GFX_3D_Renderer_Create(); + + Output_ApplyRenderSettings(); + GFX_3D_Renderer_SetPrimType(m_Renderer3D, GFX_3D_PRIM_TRI); + GFX_3D_Renderer_SetAlphaThreshold(m_Renderer3D, 0.0); + GFX_3D_Renderer_SetAlphaPointDiscard(m_Renderer3D, true); + Output_Textures_Init(); - return S_Output_Init(); + Output_Meshes_Init(); + Output_Sprites_Init(); + return true; } void Output_Shutdown(void) { + Output_Meshes_Shutdown(); Output_Sprites_Shutdown(); Output_Textures_Shutdown(); - S_Output_Shutdown(); - Memory_FreePointer(&m_BackdropImagePath); + + M_ReleaseTextures(); + M_ReleaseSurfaces(); + + if (m_Renderer2D != nullptr) { + GFX_2D_Renderer_Destroy(m_Renderer2D); + m_Renderer2D = nullptr; + } + if (m_Renderer3D != nullptr) { + GFX_3D_Renderer_Destroy(m_Renderer3D); + m_Renderer3D = nullptr; + } + GFX_Context_Detach(); + Output_ClearLastBackgroundPath(); } -void Output_ReserveVertexBuffer(const size_t size) +void Output_SetWindowSize(int32_t width, int32_t height) { - m_VBuf = GameBuf_Alloc(size * sizeof(PHD_VBUF), GBUF_VERTEX_BUFFER); - m_EnvMapUV = GameBuf_Alloc(size * sizeof(TEXTURE_UV), GBUF_VERTEX_BUFFER); + GFX_Context_SetWindowSize(width, height); } -void Output_SetWindowSize(int width, int height) +void Output_ApplyLevelSettings(void) { - S_Output_SetWindowSize(width, height); + Output_SetWaterColor(Level_GetWaterColor()); + Output_SetFogStart(Level_GetFogStart() * WALL_L); + Output_SetFogEnd(Level_GetFogEnd() * WALL_L); } void Output_ApplyRenderSettings(void) { - S_Output_ApplyRenderSettings(); - if (m_BackdropImagePath) { - Output_LoadBackgroundFromFile(m_BackdropImagePath); + Output_Textures_ApplyRenderSettings(); + Output_ApplyLevelSettings(); + + if (m_Renderer3D == nullptr) { + return; + } + + if (m_PictureSurface != nullptr + && (Screen_GetResWidth() != m_SurfaceWidth + || Screen_GetResHeight() != m_SurfaceHeight)) { + GFX_2D_Surface_Free(m_PictureSurface); + m_PictureSurface = nullptr; + } + + m_SurfaceWidth = Screen_GetResWidth(); + m_SurfaceHeight = Screen_GetResHeight(); + + GFX_Context_SetVSync(g_Config.rendering.enable_vsync); + GFX_Context_SetDisplayFilter(g_Config.rendering.fbo_filter); + GFX_Context_SetDisplaySize(m_SurfaceWidth, m_SurfaceHeight); + GFX_Context_SetRenderingMode(g_Config.rendering.render_mode); + GFX_Context_SetWireframeMode(g_Config.rendering.enable_wireframe); + GFX_Context_SetLineWidth(g_Config.rendering.wireframe_width); + GFX_3D_Renderer_SetAnisotropyFilter( + m_Renderer3D, g_Config.rendering.anisotropy_filter); + + const char *const last_path = Output_GetLastBackgroundPath(); + if (last_path != nullptr) { + Output_LoadBackgroundFromFile(last_path); } } -void Output_DownloadTextures(void) +void Output_ObserveLevelLoad(void) { - S_Output_DownloadTextures(Output_GetTexturePageCount()); - + M_DownloadTextures(Output_GetTexturePageCount()); Output_Textures_ObserveLevelLoad(); Output_Sprites_ObserveLevelLoad(); + Output_Meshes_ObserveLevelLoad(); + + Output_ApplyLevelSettings(); } -void Output_DrawBlack(void) +void Output_ObserveLevelUnload(void) { - Output_DrawBlackRectangle(255); + Output_Meshes_ObserveLevelUnload(); +} + +void Output_ObserveRoomFlip(const ROOM *room) +{ + Output_Meshes_ObserveRoomFlip(room); } void Output_FlushTranslucentObjects(void) { + Output_RememberState(); // draw transparent lightnings as last in the 3D geometry pipeline for (int32_t i = 0; i < m_LightningCount; i++) { - const LIGHTNING *const lightning = &m_LightningTable[i]; - S_Output_DrawLightningSegment( - lightning->edges[0].pos.x, lightning->edges[0].pos.y, - lightning->edges[0].pos.z, lightning->edges[0].thickness, - lightning->edges[1].pos.x, lightning->edges[1].pos.y, - lightning->edges[1].pos.z, lightning->edges[1].thickness); + M_DrawLightningSegment(&m_LightningTable[i]); } + Output_RestoreState(); } void Output_BeginScene(void) @@ -613,478 +506,283 @@ void Output_BeginScene(void) Output_ApplyFOV(); Text_DrawReset(); + Output_RememberState(); Output_Sprites_RenderBegin(); - S_Output_RenderBegin(); + Output_Meshes_RenderBegin(); + + GFX_Context_Clear(); + GFX_Track_Reset(); + GFX_3D_Renderer_RenderBegin(m_Renderer3D); + GFX_3D_Renderer_SetTextureFilter( + m_Renderer3D, g_Config.rendering.texture_filter); + GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); m_LightningCount = 0; } void Output_EndScene(void) { - S_Output_DisableDepthTest(); - S_Output_ClearDepthBuffer(); + M_DisableDepthTest(); + Output_ClearDepthBuffer(); Overlay_DrawFPSInfo(); - S_Output_EnableDepthTest(); - S_Output_RenderEnd(); - S_Output_FlipScreen(); + M_EnableDepthTest(); + GFX_3D_Renderer_RenderEnd(m_Renderer3D); + M_FlipScreen(); Shell_ProcessEvents(); g_FPSCounter++; } void Output_ClearDepthBuffer(void) { - S_Output_ClearDepthBuffer(); -} - -void Output_DrawObjectMesh(const OBJECT_MESH *const mesh, const int32_t clip) -{ - if (!M_CalcObjectVertices(mesh->vertices, mesh->num_vertices)) { - return; - } - - M_CalcVerticeLight(mesh); - M_DrawTexturedFace4s(mesh->tex_face4s, mesh->num_tex_face4s); - M_DrawTexturedFace3s(mesh->tex_face3s, mesh->num_tex_face3s); - M_DrawFlatFace4s(mesh->flat_face4s, mesh->num_flat_face4s); - M_DrawFlatFace3s(mesh->flat_face3s, mesh->num_flat_face3s); - - if (mesh->enable_reflections && g_Config.visuals.enable_reflections) { - if (!M_CalcVerticeEnvMap(mesh)) { - return; - } - - M_DrawObjectFace4EnvMap(mesh->tex_face4s, mesh->num_tex_face4s); - M_DrawObjectFace3EnvMap(mesh->tex_face3s, mesh->num_tex_face3s); - M_DrawObjectFace4EnvMap(mesh->flat_face4s, mesh->num_flat_face4s); - M_DrawObjectFace3EnvMap(mesh->flat_face3s, mesh->num_flat_face3s); - } - - if (g_Config.rendering.enable_debug_spheres) { - M_DrawSphere( - (XYZ_32) { - .x = mesh->center.x, - .y = mesh->center.y, - .z = mesh->center.z, - }, - mesh->radius); - } -} - -void Output_DrawObjectMesh_I(const OBJECT_MESH *const mesh, const int32_t clip) -{ - Matrix_Push(); - Matrix_Interpolate(); - Output_DrawObjectMesh(mesh, clip); - Matrix_Pop(); -} - -void Output_SetSkyboxEnabled(const bool enabled) -{ - m_IsSkyboxEnabled = enabled; -} - -bool Output_IsSkyboxEnabled(void) -{ - return m_IsSkyboxEnabled; -} - -void Output_DrawSkybox(const OBJECT_MESH *const mesh) -{ - g_PhdLeft = Viewport_GetMinX(); - g_PhdTop = Viewport_GetMinY(); - g_PhdRight = Viewport_GetMaxX(); - g_PhdBottom = Viewport_GetMaxY(); - - if (!M_CalcObjectVertices(mesh->vertices, mesh->num_vertices)) { - return; - } - - S_Output_DisableDepthTest(); - M_CalcSkyboxLight(mesh); - M_DrawTexturedFace4s(mesh->tex_face4s, mesh->num_tex_face4s); - M_DrawTexturedFace3s(mesh->tex_face3s, mesh->num_tex_face3s); - M_DrawFlatFace4s(mesh->flat_face4s, mesh->num_flat_face4s); - M_DrawFlatFace3s(mesh->flat_face3s, mesh->num_flat_face3s); - S_Output_EnableDepthTest(); -} - -void Output_DrawRoomMesh(const ROOM *const room) -{ - const ROOM_MESH *const mesh = &room->mesh; - M_CalcRoomVertices(mesh); - - if (m_IsWibbleEffect) { - S_Output_DisableDepthWrites(); - M_DrawTexturedFace4s(mesh->face4s, mesh->num_face4s); - M_DrawTexturedFace3s(mesh->face3s, mesh->num_face3s); - S_Output_EnableDepthWrites(); - M_CalcRoomVerticesWibble(mesh); - } - - M_DrawTexturedFace4s(mesh->face4s, mesh->num_face4s); - M_DrawTexturedFace3s(mesh->face3s, mesh->num_face3s); -} - -void Output_DrawRoomPortals(const ROOM *const room) -{ - S_Output_DisableDepthTest(); - const RGBA_8888 portal_color = { 0, 0, 255, 255 }; - for (int32_t i = 0; i < room->portals->count; i++) { - const PORTAL *const portal = &room->portals->portal[i]; - const XYZ_32 vertices[4] = { - { portal->vertex[0].x, portal->vertex[0].y, portal->vertex[0].z }, - { portal->vertex[1].x, portal->vertex[1].y, portal->vertex[1].z }, - { portal->vertex[2].x, portal->vertex[2].y, portal->vertex[2].z }, - { portal->vertex[3].x, portal->vertex[3].y, portal->vertex[3].z }, - }; - Output_Draw3DFrame(vertices, portal_color); - } - S_Output_EnableDepthTest(); -} - -void Output_DrawRoomTriggers(const ROOM *const room) -{ -#define DRAW_TRI(a, b, c, color) \ - do { \ - S_Output_DrawFlatTriangle(a, b, c, color); \ - S_Output_DrawFlatTriangle(c, b, a, color); \ - } while (0) -#define DRAW_QUAD(a, b, c, d, color) \ - do { \ - DRAW_TRI(a, b, d, color); \ - DRAW_TRI(b, c, d, color); \ - } while (0) - - const RGBA_8888 color = { .r = 255, .g = 0, .b = 255, .a = 128 }; - const XZ_16 offsets[4] = { { 0, 0 }, { 0, 1 }, { 1, 1 }, { 1, 0 } }; - - m_IsWaterEffect = false; - m_IsShadeEffect = false; - S_Output_DisableTextureMode(); - S_Output_DisableDepthWrites(); - S_Output_SetBlendingMode(GFX_BLEND_MODE_NORMAL); - for (int32_t z = 0; z < room->size.z; z++) { - for (int32_t x = 0; x < room->size.x; x++) { - const SECTOR *sector = Room_GetUnitSector(room, x, z); - if (sector->trigger == nullptr) { - continue; - } - PHD_VBUF vns[4]; - for (int32_t i = 0; i < 4; i++) { - XYZ_16 vertex_pos = { - .x = (x + offsets[i].x) * WALL_L, - .z = (z + offsets[i].z) * WALL_L, - }; - XYZ_32 world_pos = { - .x = room->pos.x + x * WALL_L + offsets[i].x * (WALL_L - 1), - .z = room->pos.z + z * WALL_L + offsets[i].z * (WALL_L - 1), - .y = room->pos.y, - }; - - int16_t room_num = room - Room_Get(0); - sector = Room_GetSector( - world_pos.x, world_pos.y, world_pos.z, &room_num); - vertex_pos.y = - Room_GetHeight( - sector, world_pos.x, world_pos.y, world_pos.z) - + (m_IsWaterEffect ? -16 : -2); - - M_CalcVertex(&vns[i], vertex_pos); - vns[i].g = HIGH_LIGHT; - vns[i].zv -= - (double)((1 << W2V_SHIFT) / 2); // reduce z fighting - } - - DRAW_QUAD(&vns[0], &vns[1], &vns[2], &vns[3], color); - } - } - - S_Output_SetBlendingMode(GFX_BLEND_MODE_OFF); - S_Output_EnableDepthWrites(); - -#undef DRAW_TRI -#undef DRAW_QUAD -} - -void Output_DrawShadow( - const int16_t size, const BOUNDS_16 *const bounds, const ITEM *const item) -{ - if (!item->enable_shadow) { - return; - } - - SHADOW_INFO shadow = {}; - shadow.vertex_count = g_Config.visuals.enable_round_shadow ? 32 : 8; - - int32_t x0 = bounds->min.x; - int32_t x1 = bounds->max.x; - int32_t z0 = bounds->min.z; - int32_t z1 = bounds->max.z; - - int32_t x_mid = (x0 + x1) / 2; - int32_t z_mid = (z0 + z1) / 2; - - int32_t x_add = (x1 - x0) * size / 1024; - int32_t z_add = (z1 - z0) * size / 1024; - - for (int32_t i = 0; i < shadow.vertex_count; i++) { - int32_t angle = (DEG_180 + i * DEG_360) / shadow.vertex_count; - shadow.vertices[i].x = x_mid + (x_add * 2) * Math_Sin(angle) / DEG_90; - shadow.vertices[i].z = z_mid + (z_add * 2) * Math_Cos(angle) / DEG_90; - shadow.vertices[i].y = 0; - } - - Matrix_Push(); - Matrix_TranslateAbs( - item->interp.result.pos.x, item->floor, item->interp.result.pos.z); - Matrix_RotY(item->rot.y); - - if (M_CalcObjectVertices(shadow.vertices, shadow.vertex_count)) { - int16_t clip_and = 1; - int16_t clip_positive = 1; - int16_t clip_or = 0; - for (int32_t i = 0; i < shadow.vertex_count; i++) { - clip_and &= m_VBuf[i].clip; - clip_positive &= m_VBuf[i].clip >= 0; - clip_or |= m_VBuf[i].clip; - } - PHD_VBUF *vn1 = &m_VBuf[0]; - PHD_VBUF *vn2 = &m_VBuf[g_Config.visuals.enable_round_shadow ? 4 : 1]; - PHD_VBUF *vn3 = &m_VBuf[g_Config.visuals.enable_round_shadow ? 8 : 2]; - - int32_t c1 = (vn3->xs - vn2->xs) * (vn1->ys - vn2->ys); - int32_t c2 = (vn1->xs - vn2->xs) * (vn3->ys - vn2->ys); - bool visible = (int32_t)(c1 - c2) >= 0; - - if (!clip_and && clip_positive && visible) { - S_Output_DrawShadow( - &m_VBuf[0], clip_or ? 1 : 0, shadow.vertex_count); - } - } - - Matrix_Pop(); -} - -int32_t Output_GetDrawDistMin(void) -{ - return 20; -} - -int32_t Output_GetDrawDistFade(void) -{ - return m_DrawDistFade; -} - -int32_t Output_GetDrawDistMax(void) -{ - return m_DrawDistMax; -} - -void Output_SetDrawDistFade(int32_t dist) -{ - m_DrawDistFade = dist; -} - -void Output_SetDrawDistMax(int32_t dist) -{ - m_DrawDistMax = dist; - - const double near_z = Output_GetNearZ(); - const double far_z = Output_GetFarZ(); - const double res_z = 0.99 * near_z * far_z / (far_z - near_z); - g_FltResZ = res_z; - g_FltResZBuf = 0.005 + res_z / near_z; -} - -void Output_SetWaterColor(const RGB_F *color) -{ - m_WaterColor.r = color->r; - m_WaterColor.g = color->g; - m_WaterColor.b = color->b; -} - -void Output_SetLightAdder(const int32_t adder) -{ - m_LsAdder = adder; -} - -int32_t Output_GetLightAdder(void) -{ - return m_LsAdder; -} - -void Output_SetLightDivider(const int32_t divider) -{ - m_LsDivider = divider; -} - -int32_t Output_GetNearZ(void) -{ - return Output_GetDrawDistMin() << W2V_SHIFT; -} - -int32_t Output_GetFarZ(void) -{ - return Output_GetDrawDistMax() << W2V_SHIFT; -} - -void Output_DrawSprite( - const int32_t x, const int32_t y, const int32_t z, const int16_t sprite_idx, - const int16_t shade) -{ - Matrix_Push(); - Matrix_TranslateAbs(x, y, z); - Output_Sprites_RenderSingleSprite( - g_MatrixPtr, (XYZ_32) { 0, 0, 0 }, sprite_idx, shade); - Matrix_Pop(); + GFX_3D_Renderer_ClearDepth(m_Renderer3D); } void Output_DrawScreenFlatQuad( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 color) + const int32_t sx, const int32_t sy, const int32_t w, const int32_t h, + const RGBA_8888 color) { - S_Output_Draw2DQuad(sx, sy, sx + w, sy + h, color, color, color, color); + M_Draw2DQuad(sx, sy, sx + w, sy + h, color, color, color, color); } void Output_DrawScreenGradientQuad( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 tl, RGBA_8888 tr, - RGBA_8888 bl, RGBA_8888 br) + const int32_t sx, const int32_t sy, const int32_t w, const int32_t h, + const RGBA_8888 tl, const RGBA_8888 tr, const RGBA_8888 bl, + const RGBA_8888 br) { - S_Output_Draw2DQuad(sx, sy, sx + w, sy + h, tl, tr, bl, br); -} - -void Output_Draw3DLine( - const XYZ_32 pos_0, const XYZ_32 pos_1, const RGBA_8888 color) -{ - PHD_VBUF vbuf[2]; - uint16_t total_clip = 0xFFFF; - total_clip &= M_CalcVertex( - &vbuf[0], (XYZ_16) { .x = pos_0.x, .y = pos_0.y, .z = pos_0.z }); - total_clip &= M_CalcVertex( - &vbuf[1], (XYZ_16) { .x = pos_1.x, .y = pos_1.y, .z = pos_1.z }); - S_Output_Draw3DLine(&vbuf[0], &vbuf[1], color); -} - -void Output_Draw3DFrame(const XYZ_32 vert[4], const RGBA_8888 color) -{ - Output_Draw3DLine(vert[0], vert[1], color); - Output_Draw3DLine(vert[1], vert[2], color); - Output_Draw3DLine(vert[2], vert[3], color); - Output_Draw3DLine(vert[3], vert[0], color); + M_Draw2DQuad(sx, sy, sx + w, sy + h, tl, tr, bl, br); } void Output_DrawScreenFrame( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 colDark, - RGBA_8888 colLight, int32_t thickness) + int32_t sx, int32_t sy, const int32_t w, const int32_t h, + const RGBA_8888 col_dark, const RGBA_8888 col_light, + const int32_t thickness_i) { - float scale = Viewport_GetHeight() / 480.0; - S_Output_DrawScreenFrame( - sx - scale, sy - scale, w, h, colDark, colLight, - thickness * scale / 2.0f); + const float scale = Viewport_GetHeight() / 480.0; + const float thickness = thickness_i * scale / 2.0f; + sx -= scale; + sy -= scale; + +#define SB_NUM_VERTS_DARK 12 +#define SB_NUM_VERTS_LIGHT 10 + GFX_3D_VERTEX vertices[SB_NUM_VERTS_DARK + SB_NUM_VERTS_LIGHT]; + GFX_3D_VERTEX *const dark_vertices = vertices; + GFX_3D_VERTEX *const light_vertices = vertices + SB_NUM_VERTS_DARK; + const float sxf = sx + thickness; + const float syf = sy + thickness; + const float hf = h; + const float wf = w; + +#define SET(i, x_, y_) \ + vertices[i].x = x_; \ + vertices[i].y = y_; + // clang-format off + // Top Left Dark edge + SET(0, sxf, syf + hf - thickness); + SET(1, sxf + thickness, syf + hf - thickness); + SET(2, sxf, syf); + SET(3, sxf + thickness, syf + thickness); + SET(4, sxf + wf - thickness, syf); + SET(5, sxf + wf - thickness, syf + thickness); + // Bottom Right Dark set + SET(6, sxf + wf + thickness, syf - thickness); + SET(7, sxf + wf, syf - thickness); + SET(8, sxf + wf + thickness, syf + hf + thickness); + SET(9, sxf + wf, syf + hf); + SET(10, sxf - thickness, syf + hf + thickness); + SET(11, sxf - thickness, syf + hf); + // Light box + SET(12, sxf - thickness, syf + hf); + SET(13, sxf - thickness + thickness, syf + hf - thickness); + SET(14, sxf - thickness, syf - thickness); + SET(15, sxf, syf); + SET(16, sxf + wf, syf - thickness); + SET(17, sxf + wf - thickness, syf); + SET(18, sxf + wf, syf + hf); + SET(19, sxf + wf - thickness, syf + hf - thickness); + SET(20, sxf - thickness, syf + hf); + SET(21, sxf - thickness + thickness, syf + hf - thickness); + // clang-format on +#undef SET + + for (int32_t i = 0; i < SB_NUM_VERTS_DARK + SB_NUM_VERTS_LIGHT; i++) { + vertices[i].z = 1.0f; + vertices[i].s = 0.0f; + vertices[i].t = 0.0f; + vertices[i].w = 0.0f; + } + for (int32_t i = 0; i < SB_NUM_VERTS_DARK; i++) { + dark_vertices[i].r = col_dark.r; + dark_vertices[i].g = col_dark.g; + dark_vertices[i].b = col_dark.b; + dark_vertices[i].a = col_dark.a; + } + for (int32_t i = 0; i < SB_NUM_VERTS_LIGHT; i++) { + light_vertices[i].r = col_light.r; + light_vertices[i].g = col_light.g; + light_vertices[i].b = col_light.b; + light_vertices[i].a = col_light.a; + } + M_DisableTextureMode(); + M_DrawTriangleStrip(vertices, SB_NUM_VERTS_DARK + SB_NUM_VERTS_LIGHT); } void Output_DrawGradientScreenBox( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 tl, RGBA_8888 tr, - RGBA_8888 bl, RGBA_8888 br, int32_t thickness) + int32_t sx, int32_t sy, int32_t w, int32_t h, const RGBA_8888 tl, + const RGBA_8888 tr, const RGBA_8888 bl, const RGBA_8888 br, + const int32_t thickness_i) { - float scale = Viewport_GetHeight() / 480.0; - S_Output_4ColourTextBox( - sx - scale, sy - scale, w + scale, h + scale, tl, tr, bl, br, - thickness * scale / 2.0f); + const float scale = Viewport_GetHeight() / 480.0; + const float thickness = thickness_i * scale / 2.0f; + sx -= scale; + sy -= scale; + w += scale; + h += scale; + + // 0 2 + // * & + // 1 3 + // + // 7 5 + // # @ + // 6 4 + GFX_3D_VERTEX vertices[10]; + +#define SET(i, x_, y_, color) \ + vertices[i].x = x_; \ + vertices[i].y = y_; \ + vertices[i].r = color.r; \ + vertices[i].g = color.g; \ + vertices[i].b = color.b; \ + vertices[i].a = color.a; + // clang-format off + SET(0, sx - thickness, sy - thickness, tl); + SET(1, sx + thickness, sy + thickness, tl); + SET(2, sx + w + thickness, sy - thickness, tr); + SET(3, sx + w - thickness, sy + thickness, tr); + SET(4, sx + w + thickness, sy + h + thickness, br); + SET(5, sx + w - thickness, sy + h - thickness, br); + SET(6, sx - thickness, sy + h + thickness, bl); + SET(7, sx + thickness, sy + h - thickness, bl); + SET(8, sx - thickness, sy - thickness, tl); + SET(9, sx + thickness, sy + thickness, tl); + // clang-format on +#undef SET + + for (int32_t i = 0; i < 10; i++) { + vertices[i].z = 1.0f; + vertices[i].s = 0.0f; + vertices[i].t = 0.0f; + vertices[i].w = 0.0f; + } + M_DisableTextureMode(); + M_DrawTriangleStrip(vertices, 10); } void Output_DrawCentreGradientScreenBox( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 edge, - RGBA_8888 center, int32_t thickness) + int32_t sx, int32_t sy, int32_t w, int32_t h, const RGBA_8888 edge, + const RGBA_8888 center, const int32_t thickness_i) { - float scale = Viewport_GetHeight() / 480.0; - S_Output_2ToneColourTextBox( - sx - scale, sy - scale, w + scale, h + scale, edge, center, - thickness * scale / 2.0f); + const float scale = Viewport_GetHeight() / 480.0; + const float thickness = thickness_i * scale / 2.0f; + sx -= scale; + sy -= scale; + w += scale; + h += scale; + + // 0 2 4 + // * & + // 1 3 5 + // + // 14 15 7 6 + // + // 13 10 9 + // # @ + // 12 11 8 + + const int32_t half_w = w / 2; + const int32_t half_h = h / 2; + GFX_3D_VERTEX vertices[18]; + +#define SET(i, x_, y_, color) \ + vertices[i].x = x_; \ + vertices[i].y = y_; \ + vertices[i].r = color.r; \ + vertices[i].g = color.g; \ + vertices[i].b = color.b; \ + vertices[i].a = color.a; + // clang-format off + SET(0, sx - thickness, sy - thickness, edge); + SET(1, sx + thickness, sy + thickness, edge); + SET(2, sx + half_w, sy - thickness, center); + SET(3, sx + half_w, sy + thickness, center); + SET(4, sx + w + thickness, sy - thickness, edge); + SET(5, sx + w - thickness, sy + thickness, edge); + SET(6, sx + w + thickness, sy + half_h, center); + SET(7, sx + w - thickness, sy + half_h, center); + SET(8, sx + w + thickness, sy + h + thickness, edge); + SET(9, sx + w - thickness, sy + h - thickness, edge); + SET(10, sx + half_w, sy + h + thickness, center); + SET(11, sx + half_w, sy + h - thickness, center); + SET(12, sx - thickness, sy + h + thickness, edge); + SET(13, sx + thickness, sy + h - thickness, edge); + SET(14, sx - thickness, sy + half_h, center); + SET(15, sx + thickness, sy + half_h, center); + SET(16, sx - thickness, sy - thickness, edge); + SET(17, sx + thickness, sy + thickness, edge); + // clang-format on +#undef SET + + for (int32_t i = 0; i < 18; i++) { + vertices[i].z = 1.0f; + vertices[i].s = 0.0f; + vertices[i].t = 0.0f; + vertices[i].w = 0.0f; + } + M_DisableTextureMode(); + M_DrawTriangleStrip(vertices, 18); } -void Output_DrawScreenFBox(int32_t sx, int32_t sy, int32_t w, int32_t h) +void Output_DrawScreenFBox( + const int32_t sx, const int32_t sy, const int32_t w, const int32_t h) { RGBA_8888 color = { 0, 0, 0, 128 }; - S_Output_Draw2DQuad(sx, sy, sx + w, sy + h, color, color, color, color); + M_Draw2DQuad(sx, sy, sx + w, sy + h, color, color, color, color); } void Output_DrawScreenSprite( - int32_t sx, int32_t sy, int32_t z, int32_t scale_h, int32_t scale_v, - int32_t sprnum, int16_t shade, uint16_t flags, int32_t page) + const int32_t sx, const int32_t sy, const int32_t z, const int32_t scale_h, + const int32_t scale_v, const int32_t sprite_idx, const int16_t shade, + const uint16_t flags, const int32_t page) { - const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprnum); + const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_idx); const int32_t x0 = sx + (scale_h * sprite->x0 / PHD_ONE); const int32_t x1 = sx + (scale_h * sprite->x1 / PHD_ONE); const int32_t y0 = sy + (scale_v * sprite->y0 / PHD_ONE); const int32_t y1 = sy + (scale_v * sprite->y1 / PHD_ONE); if (x1 >= 0 && y1 >= 0 && x0 < Viewport_GetWidth() && y0 < Viewport_GetHeight()) { - S_Output_DrawSprite(x0, y0, x1, y1, Output_GetNearZ() + 200, sprnum, 0); - } -} - -void Output_DrawSpriteRel( - int32_t x, int32_t y, int32_t z, int16_t sprnum, int16_t shade) -{ - int32_t zv = g_MatrixPtr->_20 * x + g_MatrixPtr->_21 * y - + g_MatrixPtr->_22 * z + g_MatrixPtr->_23; - if (zv < Output_GetNearZ() || zv > Output_GetFarZ()) { - return; - } - - int32_t xv = g_MatrixPtr->_00 * x + g_MatrixPtr->_01 * y - + g_MatrixPtr->_02 * z + g_MatrixPtr->_03; - int32_t yv = g_MatrixPtr->_10 * x + g_MatrixPtr->_11 * y - + g_MatrixPtr->_12 * z + g_MatrixPtr->_13; - int32_t zp = zv / g_PhdPersp; - - const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprnum); - const int32_t x0 = - Viewport_GetCenterX() + (xv + (sprite->x0 << W2V_SHIFT)) / zp; - const int32_t y0 = - Viewport_GetCenterY() + (yv + (sprite->y0 << W2V_SHIFT)) / zp; - const int32_t x1 = - Viewport_GetCenterX() + (xv + (sprite->y0 << W2V_SHIFT)) / zp; - const int32_t y1 = - Viewport_GetCenterY() + (yv + (sprite->y1 << W2V_SHIFT)) / zp; - if (x1 >= Viewport_GetMinX() && y1 >= Viewport_GetMinY() - && x0 <= Viewport_GetMaxX() && y0 <= Viewport_GetMaxY()) { - int32_t depth = zv >> W2V_SHIFT; - shade += Output_CalcFogShade(depth); - CLAMPG(shade, 0x1FFF); - S_Output_DrawSprite(x0, y0, x1, y1, zv, sprnum, shade); + M_DrawSprite(x0, y0, x1, y1, Output_GetNearZ() + 200, sprite_idx, 0); } } void Output_DrawUISprite( - int32_t x, int32_t y, int32_t scale, int16_t sprnum, int16_t shade) + const int32_t x, const int32_t y, const int32_t scale, + const int16_t sprite_idx, const int16_t shade) { - const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprnum); + const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprite_idx); const int32_t x0 = x + (scale * sprite->x0 >> 16); const int32_t x1 = x + (scale * sprite->x1 >> 16); const int32_t y0 = y + (scale * sprite->y0 >> 16); const int32_t y1 = y + (scale * sprite->y1 >> 16); if (x1 >= Viewport_GetMinX() && y1 >= Viewport_GetMinY() && x0 <= Viewport_GetMaxX() && y0 <= Viewport_GetMaxY()) { - S_Output_DrawSprite( - x0, y0, x1, y1, Output_GetNearZ() + 200, sprnum, shade); + M_DrawSprite( + x0, y0, x1, y1, Output_GetNearZ() + 200, sprite_idx, shade); } } -bool Output_LoadBackgroundFromFile(const char *const path) +bool Output_LoadBackgroundFromImage(const IMAGE *const image) { - ASSERT(path != nullptr); - const char *old_path = m_BackdropImagePath; - m_BackdropImagePath = File_GuessExtension(path, m_ImageExtensions); - Memory_FreePointer(&old_path); - - IMAGE *const img = Image_CreateFromFileInto( - m_BackdropImagePath, Viewport_GetWidth(), Viewport_GetHeight(), - IMAGE_FIT_SMART); - if (img == nullptr) { - return false; - } - S_Output_DownloadBackdropSurface(img); - Image_Free(img); + M_DownloadBackdropSurface(image); return true; } @@ -1096,114 +794,48 @@ void Output_LoadBackgroundFromObject(void) void Output_UnloadBackground(void) { - S_Output_DownloadBackdropSurface(nullptr); - Memory_FreePointer(&m_BackdropImagePath); + M_DownloadBackdropSurface(nullptr); + Output_ClearLastBackgroundPath(); } void Output_DrawLightningSegment( - int32_t x1, int32_t y1, const int32_t z1, int32_t x2, int32_t y2, - const int32_t z2, const int32_t width) + const XYZ_32 pos_0, const XYZ_32 pos_1, const int32_t thickness) { if (m_LightningCount >= MAX_LIGHTNINGS) { return; } - - if (z1 < Output_GetNearZ() || z1 > Output_GetFarZ() - || z2 < Output_GetNearZ() || z2 > Output_GetFarZ()) { - return; - } - - x1 = Viewport_GetCenterX() + x1 / (z1 / g_PhdPersp); - y1 = Viewport_GetCenterY() + y1 / (z1 / g_PhdPersp); - x2 = Viewport_GetCenterX() + x2 / (z2 / g_PhdPersp); - y2 = Viewport_GetCenterY() + y2 / (z2 / g_PhdPersp); - const int32_t thickness1 = (width << W2V_SHIFT) / (z1 / g_PhdPersp); - const int32_t thickness2 = (width << W2V_SHIFT) / (z2 / g_PhdPersp); - LIGHTNING *const lightning = &m_LightningTable[m_LightningCount]; - lightning->edges[0].pos.x = x1; - lightning->edges[0].pos.y = y1; - lightning->edges[0].pos.z = z1; - lightning->edges[0].thickness = thickness1; - lightning->edges[1].pos.x = x2; - lightning->edges[1].pos.y = y2; - lightning->edges[1].pos.z = z2; - lightning->edges[1].thickness = thickness2; + lightning->pos_0 = pos_0; + lightning->pos_1 = pos_1; + lightning->thickness = thickness; m_LightningCount++; } -void Output_SetupBelowWater(bool underwater) +void Output_DrawBlackRectangle(const int32_t opacity) { - m_IsWaterEffect = true; - m_IsWibbleEffect = !underwater; - m_IsShadeEffect = true; -} - -void Output_SetupAboveWater(bool underwater) -{ - m_IsWaterEffect = false; - m_IsWibbleEffect = underwater; - m_IsShadeEffect = underwater; -} - -void Output_AnimateTextures(const int32_t num_frames) -{ - m_WibbleOffset = (m_WibbleOffset + num_frames) % WIBBLE_SIZE; - m_AnimatedTexturesOffset += num_frames; - bool update = false; - while (m_AnimatedTexturesOffset > 5) { - Output_CycleAnimatedTextures(); - update = true; - m_AnimatedTexturesOffset -= 5; - } - if (update) { - Output_Textures_Update(); - } -} - -void Output_RotateLight(int16_t pitch, int16_t yaw) -{ - int32_t cp = Math_Cos(pitch); - int32_t sp = Math_Sin(pitch); - int32_t cy = Math_Cos(yaw); - int32_t sy = Math_Sin(yaw); - int32_t ls_x = TRIGMULT2(cp, sy); - int32_t ls_y = -sp; - int32_t ls_z = TRIGMULT2(cp, cy); - m_LsVectorView.x = (g_W2VMatrix._00 * ls_x + g_W2VMatrix._01 * ls_y - + g_W2VMatrix._02 * ls_z) - >> W2V_SHIFT; - m_LsVectorView.y = (g_W2VMatrix._10 * ls_x + g_W2VMatrix._11 * ls_y - + g_W2VMatrix._12 * ls_z) - >> W2V_SHIFT; - m_LsVectorView.z = (g_W2VMatrix._20 * ls_x + g_W2VMatrix._21 * ls_y - + g_W2VMatrix._22 * ls_z) - >> W2V_SHIFT; -} - -void Output_DrawBlackRectangle(int32_t opacity) -{ - int32_t sx = 0; - int32_t sy = 0; - int32_t sw = Viewport_GetWidth(); - int32_t sh = Viewport_GetHeight(); - - RGBA_8888 background = { 0, 0, 0, opacity }; - S_Output_DisableDepthTest(); - S_Output_ClearDepthBuffer(); + const int32_t sx = 0; + const int32_t sy = 0; + const int32_t sw = Viewport_GetWidth(); + const int32_t sh = Viewport_GetHeight(); + const RGBA_8888 background = { 0, 0, 0, opacity }; + M_DisableDepthTest(); + Output_ClearDepthBuffer(); Output_DrawScreenFlatQuad(sx, sy, sw, sh, background); - S_Output_EnableDepthTest(); + M_EnableDepthTest(); } void Output_DrawBackground(void) { - // already handled in S_Output_RenderBegin + if (m_PictureSurface == nullptr) { + return; + } + GFX_2D_Renderer_Render(m_Renderer2D); } void Output_DrawPolyList(void) { // force flush the vertex stream - S_Output_ClearDepthBuffer(); + Output_ClearDepthBuffer(); } void Output_ApplyFOV(void) @@ -1232,232 +864,73 @@ void Output_ApplyFOV(void) } } -void Output_ApplyTint(float *r, float *g, float *b) +void Output_DrawTextBackground( + const UI_STYLE ui_style, const int32_t sx, const int32_t sy, int32_t w, + int32_t h, const int32_t z, const TEXT_STYLE text_style) { - const RGB_F tint = Output_GetTint(); - if (m_IsShadeEffect) { - *r *= tint.r; - *g *= tint.g; - *b *= tint.b; - } -} - -RGB_F Output_GetTint(void) -{ - if (m_IsShadeEffect) { - return m_WaterColor; - } - return (RGB_F) { 1.0f, 1.0f, 1.0f }; -} - -bool Output_MakeScreenshot(const char *const path) -{ - GFX_Context_ScheduleScreenshot(path); - return true; -} - -int Output_GetObjectBounds(const BOUNDS_16 *const bounds) -{ - if (g_MatrixPtr->_23 >= Output_GetFarZ()) { - return 0; + if (ui_style == UI_STYLE_PC) { + Output_DrawScreenFBox(sx, sy, w, h); + return; } - int32_t x_min = bounds->min.x; - int32_t x_max = bounds->max.x; - int32_t y_min = bounds->min.y; - int32_t y_max = bounds->max.y; - int32_t z_min = bounds->min.z; - int32_t z_max = bounds->max.z; + // Make sure height and width divisible by 2. + w = 2 * ((w + 1) / 2); + h = 2 * ((h + 1) / 2); + Output_DrawScreenFBox(sx - 1, sy - 1, w + 1, h + 1); - XYZ_32 vtx[8]; - vtx[0].x = x_min; - vtx[0].y = y_min; - vtx[0].z = z_min; - vtx[1].x = x_max; - vtx[1].y = y_min; - vtx[1].z = z_min; - vtx[2].x = x_max; - vtx[2].y = y_max; - vtx[2].z = z_min; - vtx[3].x = x_min; - vtx[3].y = y_max; - vtx[3].z = z_min; - vtx[4].x = x_min; - vtx[4].y = y_min; - vtx[4].z = z_max; - vtx[5].x = x_max; - vtx[5].y = y_min; - vtx[5].z = z_max; - vtx[6].x = x_max; - vtx[6].y = y_max; - vtx[6].z = z_max; - vtx[7].x = x_min; - vtx[7].y = y_max; - vtx[7].z = z_max; + QUAD_INFO gradient_quads[4] = { + { sx, sy, w / 2, h / 2 }, + { sx + w, sy, -w / 2, h / 2 }, + { sx, sy + h, w / 2, -h / 2 }, + { sx + w, sy + h, -w / 2, -h / 2 }, + }; - int num_z = 0; - x_min = 0x3FFFFFFF; - y_min = 0x3FFFFFFF; - x_max = -0x3FFFFFFF; - y_max = -0x3FFFFFFF; - - for (int i = 0; i < 8; i++) { - int32_t zv = g_MatrixPtr->_20 * vtx[i].x + g_MatrixPtr->_21 * vtx[i].y - + g_MatrixPtr->_22 * vtx[i].z + g_MatrixPtr->_23; - - if (zv > Output_GetNearZ() && zv < Output_GetFarZ()) { - ++num_z; - int32_t zp = zv / g_PhdPersp; - int32_t xv = - (g_MatrixPtr->_00 * vtx[i].x + g_MatrixPtr->_01 * vtx[i].y - + g_MatrixPtr->_02 * vtx[i].z + g_MatrixPtr->_03) - / zp; - int32_t yv = - (g_MatrixPtr->_10 * vtx[i].x + g_MatrixPtr->_11 * vtx[i].y - + g_MatrixPtr->_12 * vtx[i].z + g_MatrixPtr->_13) - / zp; - - if (x_min > xv) { - x_min = xv; - } else if (x_max < xv) { - x_max = xv; - } - - if (y_min > yv) { - y_min = yv; - } else if (y_max < yv) { - y_max = yv; - } + if (text_style == TS_HEADING) { + for (int i = 0; i < 4; i++) { + Output_DrawScreenGradientQuad( + gradient_quads[i].x, gradient_quads[i].y, gradient_quads[i].w, + gradient_quads[i].h, M_GetMenuColor(MC_BROWN_E), + M_GetMenuColor(MC_BROWN_E), M_GetMenuColor(MC_BROWN_E), + M_GetMenuColor(MC_BROWN_C)); + } + } else if (text_style == TS_REQUESTED) { + for (int i = 0; i < 4; i++) { + Output_DrawScreenGradientQuad( + gradient_quads[i].x, gradient_quads[i].y, gradient_quads[i].w, + gradient_quads[i].h, M_GetMenuColor(MC_PURPLE_E), + M_GetMenuColor(MC_PURPLE_E), M_GetMenuColor(MC_PURPLE_E), + M_GetMenuColor(MC_PURPLE_C)); } } +} - x_min += Viewport_GetCenterX(); - x_max += Viewport_GetCenterX(); - y_min += Viewport_GetCenterY(); - y_max += Viewport_GetCenterY(); - - if (!num_z || x_min > g_PhdRight || y_min > g_PhdBottom || x_max < g_PhdLeft - || y_max < g_PhdTop) { - return 0; // out of screen +void Output_DrawTextOutline( + const UI_STYLE ui_style, const int32_t sx, const int32_t sy, int32_t w, + int32_t h, const int32_t z, const TEXT_STYLE text_style) +{ + if (ui_style == UI_STYLE_PC) { + Output_DrawScreenFrame( + sx, sy, w, h, M_GetMenuColor(MC_GOLD_DARK), + M_GetMenuColor(MC_GOLD_LIGHT), TEXT_OUTLINE_THICKNESS); + return; } - if (num_z < 8 || x_min < 0 || y_min < 0 || x_max > Viewport_GetMaxX() - || y_max > Viewport_GetMaxY()) { - return -1; // clipped + if (text_style == TS_HEADING) { + Output_DrawGradientScreenBox( + sx, sy, w, h, M_GetMenuColor(MC_BLACK), M_GetMenuColor(MC_BLACK), + M_GetMenuColor(MC_BLACK), M_GetMenuColor(MC_BLACK), + TEXT_OUTLINE_THICKNESS); + } else if (text_style == TS_BACKGROUND) { + Output_DrawGradientScreenBox( + sx, sy, w, h, M_GetMenuColor(MC_GREY_TL), + M_GetMenuColor(MC_GREY_TR), M_GetMenuColor(MC_GREY_BL), + M_GetMenuColor(MC_GREY_BR), TEXT_OUTLINE_THICKNESS); + } else if (text_style == TS_REQUESTED) { + // Make sure height and width divisible by 2. + w = 2 * ((w + 1) / 2); + h = 2 * ((h + 1) / 2); + Output_DrawCentreGradientScreenBox( + sx, sy, w, h, M_GetMenuColor(MC_GREY_E), M_GetMenuColor(MC_GREY_C), + TEXT_OUTLINE_THICKNESS); } - - return 1; // fully on screen -} - -int32_t Output_CalcFogShade(const int32_t depth) -{ - int32_t fog_begin = Output_GetDrawDistFade(); - int32_t fog_end = Output_GetDrawDistMax(); - - if (depth < fog_begin) { - return 0; - } - if (depth >= fog_end) { - return 0x1FFF; - } - - return (depth - fog_begin) * 0x1FFF / (fog_end - fog_begin); -} - -int32_t Output_GetRoomLightShade(const ROOM_LIGHT_MODE mode) -{ - // TODO: remove - ASSERT_FAIL(); - return 0; -} - -void Output_LightRoomVertices(const ROOM *room) -{ - // TODO: remove - ASSERT_FAIL(); -} - -int32_t Output_GetWibbleOffset(void) -{ - if (!m_IsWibbleEffect) { - return -1; - } - return m_WibbleOffset; -} - -void Output_GetProjectionMatrix(GLfloat output[][4]) -{ - const float left = 0.0f; - const float top = 0.0f; - const float right = Viewport_GetWidth(); - const float bottom = Viewport_GetHeight(); - const float near = Output_GetNearZ() / (float)(1 << W2V_SHIFT); - const float far = Output_GetFarZ() / (float)(1 << W2V_SHIFT); - const float aspect = (float)right / (float)bottom; - const float fov = Viewport_GetFOV() * M_PI / (float)DEG_180; - const float f = 1.0f / tan(fov / 2.0f); - - output[0][0] = f / aspect; - output[0][1] = 0.0f; - output[0][2] = 0.0f; - output[0][3] = 0.0f; - - output[1][0] = 0.0f; - output[1][1] = -f; - output[1][2] = 0.0f; - output[1][3] = 0.0f; - - output[2][0] = 0.0f; - output[2][1] = 0.0f; - // TODO(dash): these do not match the original game Z buffer mapping. - // I couldn't figure out how to replicate the original MAP_DEPTH transform. - // To combat this, the vertex shaders for now fence the simpler matrices - // behind a `NEW_ZBUFFER` define, and fall back to doing the perspective - // transform completely manually. This will be removable once we no longer - // rely on PHD_VBUF / GFX_3D_Rendere for world rendering. - output[2][2] = (far + near) / (far - near); - output[2][3] = -(2.0f * far * near) / (far - near); - - output[3][0] = 0.0f; - output[3][1] = 0.0f; - output[3][2] = 1.0f; - output[3][3] = 0.0f; -} - -void Output_EnableScissor( - const float x, const float y, const float w, const float h) -{ - const int32_t border = 2; // to deal with precision issues - - float scale_x; - float scale_y; - GFX_Context_GetScale(&scale_x, &scale_y); - - glEnable(GL_SCISSOR_TEST); - glScissor( - x * scale_x, (GFX_Context_GetDisplayHeight() - y) * scale_y, - w * scale_x, h * scale_y); -} - -void Output_DisableScissor(void) -{ - glDisable(GL_SCISSOR_TEST); -} - -void Output_RememberState(void) -{ - glGetIntegerv(GL_CURRENT_PROGRAM, &m_CachedState.bound_program); - glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &m_CachedState.bound_vao); - glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &m_CachedState.bound_vbo); - glGetIntegerv(GL_TEXTURE_BINDING_2D, &m_CachedState.bound_texture); - GFX_GL_CheckError(); -} - -void Output_RestoreState(void) -{ - glBindVertexArray(m_CachedState.bound_vao); - glBindBuffer(GL_ARRAY_BUFFER, m_CachedState.bound_vbo); - glBindTexture(GL_TEXTURE_2D, m_CachedState.bound_texture); - glUseProgram(m_CachedState.bound_program); - GFX_GL_CheckError(); } diff --git a/src/tr1/game/output.h b/src/tr1/game/output.h index be285ccb3..6a1bdd517 100644 --- a/src/tr1/game/output.h +++ b/src/tr1/game/output.h @@ -10,43 +10,31 @@ bool Output_Init(void); void Output_Shutdown(void); -void Output_ReserveVertexBuffer(size_t size); -void Output_SetWindowSize(int width, int height); +void Output_SetWindowSize(int32_t width, int32_t height); +void Output_ApplyLevelSettings(void); void Output_ApplyRenderSettings(void); -void Output_DownloadTextures(void); +void Output_ObserveFOVChange(void); int32_t Output_GetNearZ(void); int32_t Output_GetFarZ(void); -int32_t Output_GetDrawDistMin(void); -int32_t Output_GetDrawDistFade(void); -int32_t Output_GetDrawDistMax(void); -void Output_SetDrawDistFade(int32_t dist); -void Output_SetDrawDistMax(int32_t dist); -void Output_SetWaterColor(const RGB_F *color); +void Output_SetWaterColor(const RGB_888 color); void Output_BeginScene(void); void Output_EndScene(void); -void Output_DrawBlack(void); void Output_ClearDepthBuffer(void); - -void Output_DrawObjectMesh(const OBJECT_MESH *mesh, int32_t clip); -void Output_DrawObjectMesh_I(const OBJECT_MESH *mesh, int32_t clip); - void Output_SetSkyboxEnabled(bool enabled); bool Output_IsSkyboxEnabled(void); -void Output_DrawSkybox(const OBJECT_MESH *mesh); -void Output_DrawRoomMesh(const ROOM *mesh); +void Output_DrawSkybox(const OBJECT_MESH *mesh); +void Output_DrawObjectMesh(const OBJECT_MESH *mesh, int32_t clip); +void Output_DrawObjectMesh_I(const OBJECT_MESH *mesh, int32_t clip); +void Output_DrawRoomMesh(ROOM *mesh); void Output_DrawRoomPortals(const ROOM *room); void Output_DrawRoomTriggers(const ROOM *room); void Output_DrawShadow(int16_t size, const BOUNDS_16 *bounds, const ITEM *item); -void Output_DrawLightningSegment( - int32_t x1, int32_t y1, int32_t z1, int32_t x2, int32_t y2, int32_t z2, - int32_t width); -void Output_Draw3DLine(XYZ_32 pos_0, XYZ_32 pos_1, RGBA_8888 color); -void Output_Draw3DFrame(const XYZ_32 vert[4], RGBA_8888 color); +void Output_DrawLightningSegment(XYZ_32 pos_0, XYZ_32 pos_1, int32_t thickness); void Output_FlushTranslucentObjects(void); void Output_DrawScreenFlatQuad( @@ -68,12 +56,10 @@ void Output_DrawCentreGradientScreenBox( void Output_DrawScreenFBox(int32_t sx, int32_t sy, int32_t w, int32_t h); void Output_DrawSprite( - int32_t x, int32_t y, int32_t z, int16_t sprnum, int16_t shade); + int32_t x, int32_t y, int32_t z, int16_t sprnum, int16_t shade, RGB_F tint); void Output_DrawScreenSprite( int32_t sx, int32_t sy, int32_t z, int32_t scale_h, int32_t scale_v, int32_t sprnum, int16_t shade, uint16_t flags, int32_t page); -void Output_DrawSpriteRel( - int32_t x, int32_t y, int32_t z, int16_t sprnum, int16_t shade); void Output_DrawUISprite( int32_t x, int32_t y, int32_t scale, int16_t sprnum, int16_t shade); @@ -82,13 +68,13 @@ void Output_SetupAboveWater(bool underwater); void Output_AnimateTextures(int32_t num_frames); void Output_ApplyFOV(void); -void Output_ApplyTint(float *r, float *g, float *b); RGB_F Output_GetTint(void); -void Output_FillEnvironmentMap(void); bool Output_MakeScreenshot(const char *path); -int32_t Output_GetWibbleOffset(void); +bool Output_GetWaterEffect(void); +bool Output_GetWibbleEffect(void); +int32_t Output_GetTime(void); void Output_GetProjectionMatrix(GLfloat output[][4]); void Output_EnableScissor(float x, float y, float w, float h); void Output_DisableScissor(void); @@ -96,3 +82,6 @@ void Output_DisableScissor(void); // TODO: these functions are poor in their design and should be not needed void Output_RememberState(void); void Output_RestoreState(void); + +int32_t Output_GetLightDivider(void); +XYZ_32 Output_GetLightVectorView(void); diff --git a/src/tr1/game/output/draw_misc.c b/src/tr1/game/output/draw_misc.c new file mode 100644 index 000000000..739880a8a --- /dev/null +++ b/src/tr1/game/output/draw_misc.c @@ -0,0 +1,302 @@ +#include "game/output.h" +#include "game/output/meshes/common.h" +#include "game/output/meshes/dynamic.h" +#include "game/output/meshes/objects.h" +#include "game/output/meshes/rooms.h" +#include "game/output/sprites.h" +#include "game/output/utils.h" + +#include +#include + +#include + +static struct { + GLint bound_polygon_mode[2]; +} m_CachedState; + +static bool m_IsSkyboxEnabled = false; +static void M_DrawSphere(XYZ_32 pos, int32_t radius); + +static void M_DrawSphere(const XYZ_32 pos, const int32_t radius) +{ + // More subdivisions means smoother spheres. + const int32_t subdivisions = 12; + const int32_t position_count = SQUARE(subdivisions + 1); + XYZ_F positions[position_count]; + int32_t index = 0; + + for (int32_t i = 0; i <= subdivisions; i++) { + const float theta = (M_PI * i) / subdivisions; // Latitude angle + const float sin_theta = sinf(theta); + const float cos_theta = cosf(theta); + + for (int32_t j = 0; j <= subdivisions; j++) { + const float phi = (2 * M_PI * j) / subdivisions; // Longitude angle + const float sin_phi = sinf(phi); + const float cos_phi = cosf(phi); + + // Convert spherical coordinates to 3D points. + positions[index] = (XYZ_F) { + .x = pos.x + radius * cos_phi * sin_theta, + .y = pos.y + radius * cos_theta, + .z = pos.z + radius * sin_phi * sin_theta, + }; + index++; + } + } + + const int32_t vertex_count = + subdivisions * subdivisions * OUTPUT_QUAD_VERTICES; + OUTPUT_MESH_VERTEX vertices[vertex_count]; + OUTPUT_MESH_VERTEX *out_vertex = vertices; + for (int32_t i = 0; i < subdivisions; i++) { + for (int32_t j = 0; j < subdivisions; j++) { + const int32_t indices[4] = { + i * (subdivisions + 1) + j, + (i + 1) * (subdivisions + 1) + j, + (i + 1) * (subdivisions + 1) + (j + 1), + i * (subdivisions + 1) + (j + 1), + }; + for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { + out_vertex->pos = positions[indices[OUTPUT_QUAD_TO_FAN(k)]]; + out_vertex++; + } + } + } + + const RGBA_8888 color_black = { 0, 0, 0, 128 }; + const RGBA_8888 color_white = { 255, 255, 255, 128 }; + const bool wireframe_state = GFX_Context_GetWireframeMode(); + for (int32_t i = 0; i < vertex_count; i++) { + vertices[i].flags = + VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_CAUSTICS; + vertices[i].color = wireframe_state ? color_black : color_white; + } + + glGetIntegerv(GL_POLYGON_MODE, &m_CachedState.bound_polygon_mode[0]); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + Output_Shader_UploadMatrix(Output_Meshes_GetShader(), g_MatrixPtr); + Output_Meshes_DrawTriangles(vertex_count, vertices); + glBlendFunc(GL_ONE, GL_ZERO); + glPolygonMode(GL_FRONT_AND_BACK, m_CachedState.bound_polygon_mode[0]); +} + +void Output_SetSkyboxEnabled(const bool enabled) +{ + m_IsSkyboxEnabled = enabled; +} + +bool Output_IsSkyboxEnabled(void) +{ + return m_IsSkyboxEnabled; +} + +void Output_DrawSkybox(const OBJECT_MESH *const mesh) +{ + Output_RememberState(); + glDisable(GL_DEPTH_TEST); + Output_Meshes_RenderObjectMesh(g_MatrixPtr, Output_GetTint(), mesh); + glEnable(GL_DEPTH_TEST); + Output_RestoreState(); +} + +void Output_DrawObjectMesh(const OBJECT_MESH *const mesh, const int32_t clip) +{ + Output_RememberState(); + Output_Meshes_RenderObjectMesh(g_MatrixPtr, Output_GetTint(), mesh); + Output_RestoreState(); + + if (g_Config.rendering.enable_debug_spheres) { + M_DrawSphere( + (XYZ_32) { mesh->center.x, mesh->center.y, mesh->center.z }, + mesh->radius); + } +} + +void Output_DrawObjectMesh_I(const OBJECT_MESH *const mesh, const int32_t clip) +{ + Matrix_Push(); + Matrix_Interpolate(); + Output_DrawObjectMesh(mesh, clip); + Matrix_Pop(); +} + +void Output_DrawRoomMesh(ROOM *const room) +{ + Output_RememberState(); + Output_LightRoom(room); + Output_EnableScissor( + room->bound_left, room->bound_bottom, + room->bound_right - room->bound_left, + room->bound_bottom - room->bound_top); + Output_Meshes_RenderRoomMesh(g_MatrixPtr, Output_GetTint(), room); + Output_DisableScissor(); + Output_RestoreState(); +} + +void Output_DrawRoomPortals(const ROOM *const room) +{ + const int32_t vertex_count = room->portals->count * 8; + OUTPUT_MESH_VERTEX vertices[vertex_count]; + OUTPUT_MESH_VERTEX *out_vertex = vertices; + const RGBA_8888 color = { 0, 0, 255, 255 }; + for (int32_t i = 0; i < room->portals->count; i++) { + const PORTAL *const portal = &room->portals->portal[i]; + const XYZ_F positions[4] = { + { portal->vertex[0].x, portal->vertex[0].y, portal->vertex[0].z }, + { portal->vertex[1].x, portal->vertex[1].y, portal->vertex[1].z }, + { portal->vertex[2].x, portal->vertex[2].y, portal->vertex[2].z }, + { portal->vertex[3].x, portal->vertex[3].y, portal->vertex[3].z }, + }; + const int32_t indices[8] = { 0, 1, 1, 2, 2, 3, 3, 0 }; + for (int32_t j = 0; j < 8; j++) { + out_vertex->pos = positions[indices[j]]; + out_vertex++; + } + } + for (int32_t i = 0; i < vertex_count; i++) { + vertices[i].flags = + VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_CAUSTICS; + vertices[i].color = color; + } + + glDisable(GL_DEPTH_TEST); + glGetIntegerv(GL_POLYGON_MODE, &m_CachedState.bound_polygon_mode[0]); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + Output_Shader_UploadMatrix(Output_Meshes_GetShader(), g_MatrixPtr); + Output_Meshes_DrawPrimitives(GL_LINES, vertex_count, vertices); + glBlendFunc(GL_ONE, GL_ZERO); + glPolygonMode(GL_FRONT_AND_BACK, m_CachedState.bound_polygon_mode[0]); + glEnable(GL_DEPTH_TEST); +} + +void Output_DrawRoomTriggers(const ROOM *const room) +{ + const RGBA_8888 color = { .r = 255, .g = 0, .b = 255, .a = 128 }; + const XZ_16 offsets[4] = { { 0, 0 }, { 0, 1 }, { 1, 1 }, { 1, 0 } }; + + int32_t vertex_count = 0; + for (int32_t z = 0; z < room->size.z; z++) { + for (int32_t x = 0; x < room->size.x; x++) { + const SECTOR *sector = Room_GetUnitSector(room, x, z); + if (sector->trigger == nullptr) { + continue; + } + vertex_count += OUTPUT_QUAD_VERTICES; + } + } + + OUTPUT_MESH_VERTEX *vertices = + Memory_Alloc(vertex_count * sizeof(OUTPUT_MESH_VERTEX)); + OUTPUT_MESH_VERTEX *out_vertex = vertices; + + for (int32_t z = 0; z < room->size.z; z++) { + for (int32_t x = 0; x < room->size.x; x++) { + const SECTOR *sector = Room_GetUnitSector(room, x, z); + if (sector->trigger == nullptr) { + continue; + } + for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { + const int32_t j = OUTPUT_QUAD_TO_FAN(i); + XYZ_16 vertex_pos = { + .x = (x + offsets[j].x) * WALL_L, + .z = (z + offsets[j].z) * WALL_L, + }; + XYZ_32 world_pos = { + .x = room->pos.x + x * WALL_L + offsets[j].x * (WALL_L - 1), + .z = room->pos.z + z * WALL_L + offsets[j].z * (WALL_L - 1), + .y = room->pos.y, + }; + + int16_t room_num = room - Room_Get(0); + sector = Room_GetSector( + world_pos.x, world_pos.y, world_pos.z, &room_num); + vertex_pos.y = + Room_GetHeight( + sector, world_pos.x, world_pos.y, world_pos.z) + + (Output_GetWaterEffect() ? -16 : -2); + + out_vertex->pos = + (XYZ_F) { vertex_pos.x, vertex_pos.y, vertex_pos.z }; + out_vertex->flags = VERT_FLAT_SHADED | VERT_NO_LIGHTING; + out_vertex->color = color; + out_vertex++; + } + } + } + + glDepthMask(GL_FALSE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + Output_Shader_UploadMatrix(Output_Meshes_GetShader(), g_MatrixPtr); + Output_Meshes_DrawTriangles(vertex_count, vertices); + glBlendFunc(GL_ONE, GL_ZERO); + glDepthMask(GL_TRUE); + Memory_FreePointer(&vertices); +} + +void Output_DrawShadow( + const int16_t size, const BOUNDS_16 *const bounds, const ITEM *const item) +{ + if (!item->enable_shadow) { + return; + } + + const int32_t vertex_count = g_Config.visuals.enable_round_shadow ? 32 : 8; + const int32_t x0 = bounds->min.x; + const int32_t x1 = bounds->max.x; + const int32_t z0 = bounds->min.z; + const int32_t z1 = bounds->max.z; + const int32_t x_mid = (x0 + x1) / 2; + const int32_t z_mid = (z0 + z1) / 2; + const int32_t x_add = (x1 - x0) * size / 1024; + const int32_t z_add = (z1 - z0) * size / 1024; + + Matrix_Push(); + Matrix_TranslateAbs( + item->interp.result.pos.x, item->floor, item->interp.result.pos.z); + Matrix_RotY(item->rot.y); + + OUTPUT_MESH_VERTEX vertices[vertex_count * 3]; + for (int32_t i = 0; i < vertex_count; i++) { + for (int32_t j = 0; j < 2; j++) { + const int32_t angle = (DEG_180 + (i + j) * DEG_360) / vertex_count; + vertices[i * 3 + j].pos.x = + x_mid + ((x_add * 2) * Math_Sin(angle) >> W2V_SHIFT); + vertices[i * 3 + j].pos.z = + z_mid + ((z_add * 2) * Math_Cos(angle) >> W2V_SHIFT); + } + vertices[i * 3 + 2].pos.x = x_mid; + vertices[i * 3 + 2].pos.z = z_mid; + for (int32_t j = 0; j < 3; j++) { + vertices[i * 3 + j].pos.y = -5; + vertices[i * 3 + j].flags = + VERT_FLAT_SHADED | VERT_NO_LIGHTING | VERT_NO_CAUSTICS; + vertices[i * 3 + j].color = (RGBA_8888) { 0, 0, 0, 128 }; + } + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + Output_Shader_UploadMatrix(Output_Meshes_GetShader(), g_MatrixPtr); + Output_Meshes_DrawTriangles(vertex_count * 3, vertices); + glBlendFunc(GL_ONE, GL_ZERO); + + Matrix_Pop(); +} + +void Output_DrawSprite( + const int32_t x, const int32_t y, const int32_t z, const int16_t sprite_idx, + const int16_t shade, const RGB_F tint) +{ + Matrix_Push(); + Matrix_TranslateAbs(x, y, z); + Output_Sprites_RenderSingleSprite( + g_MatrixPtr, (XYZ_32) { 0, 0, 0 }, sprite_idx, shade, tint); + Matrix_Pop(); +} diff --git a/src/tr1/game/output/func.c b/src/tr1/game/output/func.c new file mode 100644 index 000000000..85ee749ff --- /dev/null +++ b/src/tr1/game/output/func.c @@ -0,0 +1,147 @@ +#include "game/output.h" +#include "game/output/meshes/common.h" +#include "game/output/sprites.h" +#include "game/viewport.h" +#include "global/vars.h" + +#include +#include + +#include + +static struct { + GLint bound_program; + GLint bound_vao; + GLint bound_vbo; + GLint bound_active_texture; + GLint bound_texture; + GLint bound_polygon_mode[2]; +} m_CachedState; + +void Output_ObserveFOVChange(void) +{ + Output_Meshes_UploadProjectionMatrix(); + Output_Sprites_UploadProjectionMatrix(); +} + +bool Output_MakeScreenshot(const char *const path) +{ + GFX_Context_ScheduleScreenshot(path); + return true; +} + +int32_t Output_GetObjectBounds(const BOUNDS_16 *const bounds) +{ + // TODO: remove + return 1; +} + +int32_t Output_CalcFogShade(const int32_t depth) +{ + // TODO: done in the shader + return 0; +} + +int32_t Output_GetRoomLightShade(const ROOM_LIGHT_MODE mode) +{ + // TODO: remove + ASSERT_FAIL(); + return 0; +} + +void Output_LightRoomVertices(const ROOM *const room) +{ + // TODO: remove + ASSERT_FAIL(); +} + +void Output_GetProjectionMatrix(GLfloat output[][4]) +{ + const float left = 0.0f; + const float top = 0.0f; + const float right = Viewport_GetWidth(); + const float bottom = Viewport_GetHeight(); + const float near = Output_GetNearZ() / (float)(1 << W2V_SHIFT); + const float far = Output_GetFarZ() / (float)(1 << W2V_SHIFT); + const float aspect = (float)right / (float)bottom; + const float fov = Viewport_GetFOV() * M_PI / (float)DEG_180; + const float f = 1.0f / tan(fov / 2.0f); + + output[0][0] = f / aspect; + output[0][1] = 0.0f; + output[0][2] = 0.0f; + output[0][3] = 0.0f; + + output[1][0] = 0.0f; + output[1][1] = -f; + output[1][2] = 0.0f; + output[1][3] = 0.0f; + + output[2][0] = 0.0f; + output[2][1] = 0.0f; + output[2][2] = g_FltResZBuf; + output[2][3] = -g_FltResZ / (float)(1 << W2V_SHIFT); + + output[3][0] = 0.0f; + output[3][1] = 0.0f; + output[3][2] = 1.0f; + output[3][3] = 0.0f; +} + +void Output_EnableScissor( + const float x, const float y, const float w, const float h) +{ + // Causes the rendering pipeline to discard every pixel outside of the + // specified window. The window is in game framebuffer viewport's + // coordinates; to make it work properly, we need to translate it to the + // SDL window coordinates first. + + const int32_t border = 2; // to deal with precision issues + + struct { + GLint x, y, w, h; + } game_viewport = { + .x = Viewport_GetMinX(), + .y = Viewport_GetMinY(), + .w = Viewport_GetWidth(), + .h = Viewport_GetHeight(), + }, gl_viewport, scissor; + glGetIntegerv(GL_VIEWPORT, &gl_viewport.x); + + const float scale_x = gl_viewport.w / (float)game_viewport.w; + const float scale_y = gl_viewport.h / (float)game_viewport.h; + scissor.x = gl_viewport.x + (x * scale_x) - border; + scissor.y = gl_viewport.y + (game_viewport.h - y) * scale_y - border; + scissor.w = w * scale_x + border * 2; + scissor.h = h * scale_y + border * 2; + + glEnable(GL_SCISSOR_TEST); + glScissor(scissor.x, scissor.y, scissor.w, scissor.h); +} + +void Output_DisableScissor(void) +{ + glDisable(GL_SCISSOR_TEST); +} + +void Output_RememberState(void) +{ + glGetIntegerv(GL_CURRENT_PROGRAM, &m_CachedState.bound_program); + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &m_CachedState.bound_vao); + glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &m_CachedState.bound_vbo); + glGetIntegerv(GL_ACTIVE_TEXTURE, &m_CachedState.bound_active_texture); + glGetIntegerv(GL_TEXTURE_BINDING_2D, &m_CachedState.bound_texture); + glGetIntegerv(GL_POLYGON_MODE, &m_CachedState.bound_polygon_mode[0]); + GFX_GL_CheckError(); +} + +void Output_RestoreState(void) +{ + glUseProgram(m_CachedState.bound_program); + glBindVertexArray(m_CachedState.bound_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_CachedState.bound_vbo); + glActiveTexture(m_CachedState.bound_active_texture); + glBindTexture(GL_TEXTURE_2D, m_CachedState.bound_texture); + glPolygonMode(GL_FRONT_AND_BACK, m_CachedState.bound_polygon_mode[0]); + GFX_GL_CheckError(); +} diff --git a/src/tr1/game/output/meshes/common.c b/src/tr1/game/output/meshes/common.c new file mode 100644 index 000000000..e51178d36 --- /dev/null +++ b/src/tr1/game/output/meshes/common.c @@ -0,0 +1,51 @@ +#include "game/output/meshes/common.h" + +#include "game/output/meshes/dynamic.h" +#include "game/output/meshes/objects.h" +#include "game/output/meshes/rooms.h" + +static OUTPUT_SHADER *m_Shader = nullptr; + +void Output_Meshes_Init(void) +{ + m_Shader = Output_Shader_Create("shaders/meshes.glsl"); + Output_Meshes_InitDynamic(); + Output_Meshes_InitRooms(); + Output_Meshes_InitObjects(); +} + +void Output_Meshes_Shutdown(void) +{ + Output_Meshes_ShutdownObjects(); + Output_Meshes_ShutdownRooms(); + Output_Meshes_ShutdownDynamic(); + Output_Shader_Free(m_Shader); + m_Shader = nullptr; +} + +void Output_Meshes_ObserveLevelLoad(void) +{ + Output_Meshes_ObserveLevelLoadObjects(); + Output_Meshes_ObserveLevelLoadRooms(); +} + +void Output_Meshes_ObserveLevelUnload(void) +{ + Output_Meshes_ObserveLevelUnloadObjects(); +} + +void Output_Meshes_RenderBegin(void) +{ + Output_Shader_UploadCommonUniforms(m_Shader); + Output_Shader_UploadProjectionMatrix(m_Shader); +} + +OUTPUT_SHADER *Output_Meshes_GetShader(void) +{ + return m_Shader; +} + +void Output_Meshes_UploadProjectionMatrix(void) +{ + Output_Shader_UploadProjectionMatrix(m_Shader); +} diff --git a/src/tr1/game/output/meshes/common.h b/src/tr1/game/output/meshes/common.h new file mode 100644 index 000000000..332401060 --- /dev/null +++ b/src/tr1/game/output/meshes/common.h @@ -0,0 +1,29 @@ +#pragma once + +#include "game/output/shader.h" +#include "game/output/textures.h" + +// clang-format off +#define VERT_NO_CAUSTICS 0b0000'0001 // = 0x01 +#define VERT_FLAT_SHADED 0b0000'0010 // = 0x02 +#define VERT_REFLECTIVE 0b0000'0100 // = 0x04 +#define VERT_NO_LIGHTING 0b0000'1000 // = 0x08 +#define VERT_SPRITE 0b0001'0000 // = 0x10 +// clang-format on + +typedef struct { + // attribute 2 + OUTPUT_UVW uvw; + // attribute 3 + OUTPUT_TEXTURE_SIZE texture_size; + // attribute 4 + float trapezoid_ratio[2]; +} OUTPUT_MESH_TEXTURE; + +void Output_Meshes_Init(void); +void Output_Meshes_Shutdown(void); +void Output_Meshes_UploadProjectionMatrix(void); +void Output_Meshes_ObserveLevelLoad(void); +void Output_Meshes_ObserveLevelUnload(void); +void Output_Meshes_RenderBegin(void); +OUTPUT_SHADER *Output_Meshes_GetShader(void); diff --git a/src/tr1/game/output/meshes/dynamic.c b/src/tr1/game/output/meshes/dynamic.c new file mode 100644 index 000000000..ca57a8874 --- /dev/null +++ b/src/tr1/game/output/meshes/dynamic.c @@ -0,0 +1,96 @@ +#include "game/output/meshes/dynamic.h" + +#include "game/output/meshes/common.h" + +#include +#include + +#include + +static struct { + GLuint vao; + GLuint vbo; +} m_Priv; + +static OUTPUT_SHADER *m_Shader = nullptr; + +void Output_Meshes_InitDynamic(void) +{ + m_Shader = Output_Meshes_GetShader(); + + glGenVertexArrays(1, &m_Priv.vao); + glBindVertexArray(m_Priv.vao); + + glGenBuffers(1, &m_Priv.vbo); + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.vbo); + glEnableVertexAttribArray(0); + glVertexAttribPointer( + 0, 3, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, pos)); + + glEnableVertexAttribArray(1); + glVertexAttribPointer( + 1, 3, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, normal)); + + glEnableVertexAttribArray(2); + glVertexAttribPointer( + 2, 3, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, uvw)); + + glEnableVertexAttribArray(3); + glVertexAttribPointer( + 3, 4, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, texture_size)); + + glEnableVertexAttribArray(4); + glVertexAttribPointer( + 4, 2, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, trapezoid_ratio)); + + glEnableVertexAttribArray(5); + glVertexAttribIPointer( + 5, 1, GL_UNSIGNED_SHORT, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, flags)); + + glEnableVertexAttribArray(6); + glVertexAttribPointer( + 6, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, color)); + + glEnableVertexAttribArray(7); + glVertexAttribPointer( + 7, 1, GL_UNSIGNED_SHORT, GL_FALSE, sizeof(OUTPUT_MESH_VERTEX), + (void *)(intptr_t)offsetof(OUTPUT_MESH_VERTEX, shade)); +} + +void Output_Meshes_ShutdownDynamic(void) +{ + if (m_Priv.vao != 0) { + glDeleteVertexArrays(1, &m_Priv.vao); + m_Priv.vao = 0; + } + if (m_Priv.vbo != 0) { + glDeleteBuffers(1, &m_Priv.vbo); + m_Priv.vbo = 0; + } +} + +void Output_Meshes_DrawPrimitives( + const GLuint prim_type, const int32_t count, + const OUTPUT_MESH_VERTEX *const vertices) +{ + Output_Shader_Bind(m_Shader); + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, count * sizeof(OUTPUT_MESH_VERTEX), + vertices, GL_STATIC_DRAW); + glBindVertexArray(m_Priv.vao); + glDrawArrays(prim_type, 0, count); +} + +void Output_Meshes_DrawTriangles( + const int32_t count, const OUTPUT_MESH_VERTEX *const vertices) +{ + Output_Meshes_DrawPrimitives(GL_TRIANGLES, count, vertices); +} diff --git a/src/tr1/game/output/meshes/dynamic.h b/src/tr1/game/output/meshes/dynamic.h new file mode 100644 index 000000000..a258fd334 --- /dev/null +++ b/src/tr1/game/output/meshes/dynamic.h @@ -0,0 +1,31 @@ +#pragma once + +#include "game/output/shader.h" +#include "game/output/textures.h" + +#include +#include +#include + +#pragma pack(push, 1) +typedef struct { + // clang-format off + XYZ_F pos; // attribute 0 + XYZ_F normal; // attribute 1 + OUTPUT_UVW uvw; // attribute 2 + OUTPUT_TEXTURE_SIZE texture_size; // attribute 3 + float trapezoid_ratio[2]; // attribute 4 + uint16_t flags; // attribute 5 + RGBA_8888 color; // attribute 6 + int16_t shade; // attribute 7 + // clang-format on +} OUTPUT_MESH_VERTEX; +#pragma pack(pop) + +void Output_Meshes_InitDynamic(void); +void Output_Meshes_ShutdownDynamic(void); + +void Output_Meshes_DrawPrimitives( + GLuint prim_type, int32_t count, const OUTPUT_MESH_VERTEX *vertices); +void Output_Meshes_DrawTriangles( + int32_t count, const OUTPUT_MESH_VERTEX *vertices); diff --git a/src/tr1/game/output/meshes/objects.c b/src/tr1/game/output/meshes/objects.c new file mode 100644 index 000000000..18ca7c173 --- /dev/null +++ b/src/tr1/game/output/meshes/objects.c @@ -0,0 +1,586 @@ +#include "game/output/meshes/objects.h" + +#include "game/output.h" +#include "game/output/meshes/common.h" +#include "game/output/textures.h" +#include "game/output/utils.h" +#include "game/output/vertex_range.h" + +#include +#include +#include +#include +#include + +#pragma pack(push, 1) +typedef struct { + // attribute 0 + XYZ_F pos; + // attribute 1 + XYZ_F normal; + // attribute 5 + uint16_t flags; + // attribute 6 + RGBA_8888 color; + // clang-format on +} M_MESH_VERTEX; + +// attribute 7 +typedef int16_t M_MESH_SHADE; +#pragma pack(pop) + +typedef struct { + int32_t vertex_start; + int32_t vertex_count; + VECTOR *animated_vertices; +} M_BATCH; + +static struct { + size_t vertex_count; + GLuint vao; + GLuint geom_vbo; + GLuint tex_vbo; + GLuint shade_vbo; + M_MESH_VERTEX *geom_vbo_data; + OUTPUT_MESH_TEXTURE *tex_vbo_data; + M_MESH_SHADE *shade_vbo_data; + int32_t *light_idx_map; + size_t batch_count; + M_BATCH *batches; +} m_Priv; + +static OUTPUT_SHADER *m_Shader = nullptr; + +static M_BATCH *M_GetBatch(const OBJECT_MESH *const mesh) +{ + const int16_t mesh_num = Object_GetMeshIndex(mesh); + return &m_Priv.batches[mesh_num]; +} + +static uint16_t M_GetFlags(const OBJECT_MESH *const mesh, const bool flat) +{ + uint16_t flags = 0; + if (flat) { + flags |= VERT_FLAT_SHADED; + } + if (mesh->enable_reflections) { + flags |= VERT_REFLECTIVE; + } + if (mesh->disable_lighting) { + flags |= VERT_NO_LIGHTING; + } + return flags; +} + +static void M_FillTexture( + OUTPUT_MESH_TEXTURE *const out_texture, const int32_t uvw_idx, + const float trapezoid_ratio_x, const float trapezoid_ratio_y) +{ + out_texture->uvw = Output_Textures_GetUVW(uvw_idx); + out_texture->texture_size = Output_Textures_GetAtlasSize(uvw_idx / 4); + out_texture->trapezoid_ratio[0] = trapezoid_ratio_x; + out_texture->trapezoid_ratio[1] = trapezoid_ratio_y; +} + +static void M_FillVertex( + M_MESH_VERTEX *out_vertex, const int32_t uvw_idx, + const OBJECT_MESH *const mesh, const int32_t vertex_idx, + const int32_t palette_idx) +{ + const XYZ_16 pos = mesh->vertices[vertex_idx]; + const XYZ_16 normal = mesh->lighting.normals[vertex_idx]; + out_vertex->pos = (XYZ_F) { .x = pos.x, .y = pos.y, .z = pos.z }; + out_vertex->normal = + (XYZ_F) { .x = normal.x, .y = -normal.y, .z = normal.z }; + out_vertex->flags = M_GetFlags(mesh, palette_idx >= 0); + if (palette_idx >= 0) { + out_vertex->color = + Output_RGB2RGBA(Output_GetPaletteColor8(palette_idx)); + } else { + out_vertex->color = (RGBA_8888) { 255, 255, 255, 255 }; + } +} + +static void M_FillFace4( + const OBJECT_MESH *const mesh, M_MESH_VERTEX **out_vertex, + OUTPUT_MESH_TEXTURE **out_texture, const FACE4 *const face, const bool flat) +{ + for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { + const int32_t j = OUTPUT_QUAD_TO_FAN(i); + const int32_t uvw_idx = face->texture_idx * 4 + j; + M_FillVertex( + *out_vertex, uvw_idx, mesh, face->vertices[j], + flat ? face->palette_idx : -1); + M_FillTexture( + *out_texture, uvw_idx, face->texture_zw[j].z, + face->texture_zw[j].w); + (*out_vertex)++; + (*out_texture)++; + } +} + +static void M_FillFace3( + const OBJECT_MESH *const mesh, M_MESH_VERTEX **out_vertex, + OUTPUT_MESH_TEXTURE **out_texture, const FACE3 *const face, const bool flat) +{ + const M_BATCH *const batch = M_GetBatch(mesh); + for (int32_t i = 0; i < OUTPUT_TRI_VERTICES; i++) { + const int32_t j = OUTPUT_TRI_TO_FAN(i); + const int32_t uvw_idx = face->texture_idx * 4 + j; + M_FillVertex( + *out_vertex, uvw_idx, mesh, face->vertices[j], + flat ? face->palette_idx : -1); + M_FillTexture(*out_texture, uvw_idx, 1.0f, 1.0f); + (*out_vertex)++; + (*out_texture)++; + } +} + +static void M_UpdateGeometry(const OBJECT_MESH *const mesh) +{ + const M_BATCH *const batch = M_GetBatch(mesh); + M_MESH_VERTEX *out_vertex = &m_Priv.geom_vbo_data[batch->vertex_start]; + OUTPUT_MESH_TEXTURE *out_texture = + &m_Priv.tex_vbo_data[batch->vertex_start]; + + for (int32_t i = 0; i < mesh->num_tex_face4s; i++) { + M_FillFace4( + mesh, &out_vertex, &out_texture, &mesh->tex_face4s[i], false); + } + for (int32_t i = 0; i < mesh->num_tex_face3s; i++) { + M_FillFace3( + mesh, &out_vertex, &out_texture, &mesh->tex_face3s[i], false); + } + for (int32_t i = 0; i < mesh->num_flat_face4s; i++) { + M_FillFace4( + mesh, &out_vertex, &out_texture, &mesh->flat_face4s[i], true); + } + for (int32_t i = 0; i < mesh->num_flat_face3s; i++) { + M_FillFace3( + mesh, &out_vertex, &out_texture, &mesh->flat_face3s[i], true); + } + +#define SET(v, j) light_idx_map[v] = face->vertices[j]; + int32_t *const light_idx_map = &m_Priv.light_idx_map[batch->vertex_start]; + int32_t v = 0; + for (int32_t i = 0; i < mesh->num_tex_face4s; i++) { + const FACE4 *const face = &mesh->tex_face4s[i]; + for (int32_t j = 0; j < OUTPUT_QUAD_VERTICES; j++) { + SET(v, OUTPUT_QUAD_TO_FAN(j)); + v++; + } + } + for (int32_t i = 0; i < mesh->num_tex_face3s; i++) { + const FACE3 *const face = &mesh->tex_face3s[i]; + for (int32_t j = 0; j < OUTPUT_TRI_VERTICES; j++) { + SET(v, OUTPUT_TRI_TO_FAN(j)); + v++; + } + } + for (int32_t i = 0; i < mesh->num_flat_face4s; i++) { + const FACE4 *const face = &mesh->flat_face4s[i]; + for (int32_t j = 0; j < OUTPUT_QUAD_VERTICES; j++) { + SET(v, OUTPUT_QUAD_TO_FAN(j)); + v++; + } + } + for (int32_t i = 0; i < mesh->num_flat_face3s; i++) { + const FACE3 *const face = &mesh->flat_face3s[i]; + for (int32_t j = 0; j < OUTPUT_TRI_VERTICES; j++) { + SET(v, OUTPUT_TRI_TO_FAN(j)); + v++; + } + } +#undef SET +} + +static void M_UpdateShades( + const MATRIX *const matrix, const OBJECT_MESH *const mesh) +{ + M_BATCH *const batch = M_GetBatch(mesh); + M_MESH_SHADE *const out_shades = + &m_Priv.shade_vbo_data[batch->vertex_start]; + int32_t *const light_idx_map = &m_Priv.light_idx_map[batch->vertex_start]; + + const int32_t ls_adder = Output_GetLightAdder(); + const int32_t ls_divider = Output_GetLightDivider(); + const XYZ_32 ls_vector_view = Output_GetLightVectorView(); + + if (mesh->num_lights <= 0) { + for (int32_t i = 0; i < batch->vertex_count; i++) { + const int32_t j = light_idx_map[i]; + int16_t shade = ls_adder + mesh->lighting.lights[j]; + CLAMP(shade, 0, SHADE_MAX); + out_shades[i] = shade; + } + } else if (ls_divider == 0) { + int16_t shade = ls_adder; + CLAMP(shade, 0, SHADE_MAX); + for (int32_t i = 0; i < batch->vertex_count; i++) { + out_shades[i] = shade; + } + } else { + // clang-format off + const int32_t xv = ( + matrix->_00 * ls_vector_view.x + + matrix->_10 * ls_vector_view.y + + matrix->_20 * ls_vector_view.z + ) / ls_divider; + + const int32_t yv = ( + matrix->_01 * ls_vector_view.x + + matrix->_11 * ls_vector_view.y + + matrix->_21 * ls_vector_view.z + ) / ls_divider; + + const int32_t zv = ( + matrix->_02 * ls_vector_view.x + + matrix->_12 * ls_vector_view.y + + matrix->_22 * ls_vector_view.z + ) / ls_divider; + // clang-format on + + for (int32_t i = 0; i < batch->vertex_count; i++) { + const int32_t j = light_idx_map[i]; + const XYZ_16 *const normal = &mesh->lighting.normals[j]; + int16_t shade = ls_adder + + ((normal->x * xv + normal->y * yv + normal->z * zv) >> 16); + CLAMP(shade, 0, SHADE_MAX); + out_shades[i] = shade; + } + } +} + +static void M_UpdateVertices(void) +{ + for (int32_t i = 0; i < Object_GetMeshCount(); i++) { + M_UpdateGeometry(Object_GetMesh(i)); + } + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.geom_vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, + m_Priv.vertex_count * sizeof(M_MESH_VERTEX), m_Priv.geom_vbo_data, + GL_STATIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, + m_Priv.vertex_count * sizeof(OUTPUT_MESH_TEXTURE), m_Priv.tex_vbo_data, + GL_DYNAMIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.shade_vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, + m_Priv.vertex_count * sizeof(M_MESH_SHADE), m_Priv.shade_vbo_data, + GL_DYNAMIC_DRAW); // shades are always dynamic +} + +static void M_PrepareBuffers(void) +{ + m_Priv.batch_count = Object_GetMeshCount(); + m_Priv.batches = + Memory_Realloc(m_Priv.batches, sizeof(M_BATCH) * m_Priv.batch_count); + + int32_t last_vertex = 0; + for (int32_t i = 0; i < Object_GetMeshCount(); i++) { + const OBJECT_MESH *const obj_mesh = Object_GetMesh(i); + M_BATCH *const batch = &m_Priv.batches[i]; + batch->vertex_start = last_vertex; + batch->vertex_count = 0; + batch->vertex_count += obj_mesh->num_tex_face4s * OUTPUT_QUAD_VERTICES; + batch->vertex_count += obj_mesh->num_tex_face3s * OUTPUT_TRI_VERTICES; + batch->vertex_count += obj_mesh->num_flat_face4s * OUTPUT_QUAD_VERTICES; + batch->vertex_count += obj_mesh->num_flat_face3s * OUTPUT_TRI_VERTICES; + last_vertex += batch->vertex_count; + } + m_Priv.vertex_count = last_vertex; + + int32_t current_vertex = 0; + for (int32_t i = 0; i < Object_GetMeshCount(); i++) { + const OBJECT_MESH *const obj_mesh = Object_GetMesh(i); + M_BATCH *const batch = &m_Priv.batches[i]; + batch->animated_vertices = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); + for (int32_t j = 0; j < obj_mesh->num_tex_face4s; j++) { + const FACE4 *const face = &obj_mesh->tex_face4s[j]; + if (Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + Vector_Add( + batch->animated_vertices, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = current_vertex, + .vertex_count = OUTPUT_QUAD_VERTICES, + }); + } + current_vertex += OUTPUT_QUAD_VERTICES; + } + for (int32_t j = 0; j < obj_mesh->num_tex_face3s; j++) { + const FACE3 *const face = &obj_mesh->tex_face3s[j]; + if (Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + Vector_Add( + batch->animated_vertices, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = current_vertex, + .vertex_count = OUTPUT_TRI_VERTICES, + }); + } + current_vertex += OUTPUT_TRI_VERTICES; + } + current_vertex += obj_mesh->num_flat_face4s * OUTPUT_QUAD_VERTICES; + current_vertex += obj_mesh->num_flat_face3s * OUTPUT_TRI_VERTICES; + Output_GlueVertexRanges(batch->animated_vertices); + } + + m_Priv.geom_vbo_data = + Memory_Alloc(m_Priv.vertex_count * sizeof(M_MESH_VERTEX)); + m_Priv.tex_vbo_data = + Memory_Alloc(m_Priv.vertex_count * sizeof(OUTPUT_MESH_TEXTURE)); + m_Priv.shade_vbo_data = + Memory_Alloc(m_Priv.vertex_count * sizeof(M_MESH_SHADE)); + m_Priv.light_idx_map = Memory_Alloc(m_Priv.vertex_count * sizeof(int32_t)); + + glGenVertexArrays(1, &m_Priv.vao); + glGenBuffers(1, &m_Priv.geom_vbo); + glGenBuffers(1, &m_Priv.tex_vbo); + glGenBuffers(1, &m_Priv.shade_vbo); + + glBindVertexArray(m_Priv.vao); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.geom_vbo); + // attribute 0: position + glEnableVertexAttribArray(0); + glVertexAttribPointer( + 0, 3, GL_FLOAT, GL_FALSE, sizeof(M_MESH_VERTEX), + (void *)(intptr_t)offsetof(M_MESH_VERTEX, pos)); + + // attribute 1: normal + glEnableVertexAttribArray(1); + glVertexAttribPointer( + 1, 3, GL_FLOAT, GL_FALSE, sizeof(M_MESH_VERTEX), + (void *)(intptr_t)offsetof(M_MESH_VERTEX, normal)); + + // attribute 5: flags + glEnableVertexAttribArray(5); + glVertexAttribIPointer( + 5, 1, GL_UNSIGNED_SHORT, sizeof(M_MESH_VERTEX), + (void *)(intptr_t)offsetof(M_MESH_VERTEX, flags)); + + // attribute 6: color + glEnableVertexAttribArray(6); + glVertexAttribPointer( + 6, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(M_MESH_VERTEX), + (void *)(intptr_t)offsetof(M_MESH_VERTEX, color)); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + // attribute 2: uvw + glEnableVertexAttribArray(2); + glVertexAttribPointer( + 2, 3, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_TEXTURE), + (void *)(intptr_t)offsetof(OUTPUT_MESH_TEXTURE, uvw)); + + // attribute 3: texture size + glEnableVertexAttribArray(3); + glVertexAttribPointer( + 3, 4, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_TEXTURE), + (void *)(intptr_t)offsetof(OUTPUT_MESH_TEXTURE, texture_size)); + + // attribute 4: trapezoid ratios + glEnableVertexAttribArray(4); + glVertexAttribPointer( + 4, 2, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_TEXTURE), + (void *)(intptr_t)offsetof(OUTPUT_MESH_TEXTURE, trapezoid_ratio)); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.shade_vbo); + + // attribute 7: shade + glEnableVertexAttribArray(7); + glVertexAttribPointer( + 7, 1, GL_UNSIGNED_SHORT, GL_FALSE, sizeof(M_MESH_SHADE), 0); + + M_UpdateVertices(); +} + +static void M_FreeBuffers(void) +{ + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + if (m_Priv.vao != 0) { + glDeleteVertexArrays(1, &m_Priv.vao); + m_Priv.vao = 0; + } + if (m_Priv.geom_vbo != 0) { + glDeleteBuffers(1, &m_Priv.geom_vbo); + m_Priv.geom_vbo = 0; + } + if (m_Priv.tex_vbo != 0) { + glDeleteBuffers(1, &m_Priv.tex_vbo); + m_Priv.tex_vbo = 0; + } + if (m_Priv.shade_vbo != 0) { + glDeleteBuffers(1, &m_Priv.shade_vbo); + m_Priv.shade_vbo = 0; + } + Memory_FreePointer(&m_Priv.geom_vbo_data); + Memory_FreePointer(&m_Priv.tex_vbo_data); + Memory_FreePointer(&m_Priv.shade_vbo_data); + Memory_FreePointer(&m_Priv.light_idx_map); + for (int32_t i = 0; i < (int32_t)m_Priv.batch_count; i++) { + Vector_Free(m_Priv.batches[i].animated_vertices); + } + Memory_FreePointer(&m_Priv.batches); +} + +void Output_Meshes_InitObjects(void) +{ + m_Shader = Output_Meshes_GetShader(); +} + +void Output_Meshes_ShutdownObjects(void) +{ + M_FreeBuffers(); +} + +void Output_Meshes_ObserveLevelLoadObjects(void) +{ + M_PrepareBuffers(); +} + +void Output_Meshes_ObserveLevelUnloadObjects(void) +{ + M_FreeBuffers(); +} + +static void M_FillAnimatedTextures(const OBJECT_MESH *const obj_mesh) +{ + const M_BATCH *const batch = M_GetBatch(obj_mesh); + OUTPUT_MESH_TEXTURE *out_texture = + &m_Priv.tex_vbo_data[batch->vertex_start]; + for (int32_t j = 0; j < obj_mesh->num_tex_face4s; j++) { + const FACE4 *const face = &obj_mesh->tex_face4s[j]; + if (!Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + out_texture += OUTPUT_QUAD_VERTICES; + continue; + } + for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { + const int32_t l = OUTPUT_QUAD_TO_FAN(k); + const int32_t uvw_idx = face->texture_idx * 4 + l; + M_FillTexture( + out_texture, uvw_idx, face->texture_zw[l].z, + face->texture_zw[l].w); + out_texture++; + } + } + for (int32_t j = 0; j < obj_mesh->num_tex_face3s; j++) { + const FACE3 *const face = &obj_mesh->tex_face3s[j]; + if (!Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + out_texture += OUTPUT_TRI_VERTICES; + continue; + } + for (int32_t k = 0; k < OUTPUT_TRI_VERTICES; k++) { + const int32_t l = OUTPUT_TRI_TO_FAN(k); + const int32_t uvw_idx = face->texture_idx * 4 + l; + M_FillTexture(out_texture, uvw_idx, 1.0f, 1.0f); + out_texture++; + } + } +} + +static void M_UpdateAnimedTextures(const OBJECT_MESH *const obj_mesh) +{ + const M_BATCH *const batch = M_GetBatch(obj_mesh); + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + for (int32_t i = 0; i < batch->animated_vertices->count; i++) { + const OUTPUT_VERTEX_RANGE *const range = + Vector_Get(batch->animated_vertices, i); + GFX_TRACK_DATA( + glBufferSubData, GL_ARRAY_BUFFER, + range->vertex_start * sizeof(OUTPUT_MESH_TEXTURE), + range->vertex_count * sizeof(OUTPUT_MESH_TEXTURE), + &m_Priv.tex_vbo_data[range->vertex_start]); + } +} + +void Output_Meshes_ObserveTextureAnimationObjects(void) +{ + for (int32_t i = 0; i < Object_GetMeshCount(); i++) { + const OBJECT_MESH *const obj_mesh = Object_GetMesh(i); + M_FillAnimatedTextures(obj_mesh); + M_UpdateAnimedTextures(obj_mesh); + } +} + +void Output_Meshes_ObserveObjectMeshSwap( + const OBJECT_MESH *const mesh_1, const OBJECT_MESH *const mesh_2) +{ + if (m_Priv.geom_vbo_data == nullptr) { + return; + } + + M_BATCH *const batch_1 = M_GetBatch(mesh_1); + M_BATCH *const batch_2 = M_GetBatch(mesh_2); + const M_BATCH batch_tmp = *batch_1; + *batch_1 = *batch_2; + *batch_2 = batch_tmp; + Output_Meshes_ObserveObjectMeshUpdate(mesh_1); + Output_Meshes_ObserveObjectMeshUpdate(mesh_2); +} + +void Output_Meshes_ObserveObjectMeshUpdate(const OBJECT_MESH *const mesh) +{ + if (m_Priv.geom_vbo_data == nullptr) { + return; + } + + const M_BATCH *const batch = M_GetBatch(mesh); + M_UpdateGeometry(mesh); + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.geom_vbo); + GFX_TRACK_SUBDATA( + glBufferSubData, GL_ARRAY_BUFFER, + batch->vertex_start * sizeof(M_MESH_VERTEX), + batch->vertex_count * sizeof(M_MESH_VERTEX), + &m_Priv.geom_vbo_data[batch->vertex_start]); + GFX_GL_CheckError(); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + GFX_TRACK_SUBDATA( + glBufferSubData, GL_ARRAY_BUFFER, + batch->vertex_start * sizeof(OUTPUT_MESH_TEXTURE), + batch->vertex_count * sizeof(OUTPUT_MESH_TEXTURE), + &m_Priv.tex_vbo_data[batch->vertex_start]); + GFX_GL_CheckError(); +} + +void Output_Meshes_RenderObjectMesh( + const MATRIX *const matrix, const RGB_F tint, const OBJECT_MESH *const mesh) +{ + const M_BATCH *const batch = M_GetBatch(mesh); + + M_UpdateShades(matrix, mesh); + + Output_Shader_UploadMatrix(m_Shader, matrix); + Output_Shader_UploadTint(m_Shader, tint); + GFX_GL_CheckError(); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.shade_vbo); + GFX_TRACK_SUBDATA( + glBufferSubData, GL_ARRAY_BUFFER, + batch->vertex_start * sizeof(M_MESH_SHADE), + batch->vertex_count * sizeof(M_MESH_SHADE), + &m_Priv.shade_vbo_data[batch->vertex_start]); + GFX_GL_CheckError(); + + glEnable(GL_CULL_FACE); + glBindVertexArray(m_Priv.vao); + GFX_GL_CheckError(); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, Output_Textures_GetAtlasTexture()); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, Output_Textures_GetEnvMapTexture()); + GFX_GL_CheckError(); + + Output_Shader_UploadWibbleEffect(m_Shader, false); + glDrawArrays(GL_TRIANGLES, batch->vertex_start, batch->vertex_count); + + glDisable(GL_CULL_FACE); + GFX_GL_CheckError(); +} diff --git a/src/tr1/game/output/meshes/objects.h b/src/tr1/game/output/meshes/objects.h new file mode 100644 index 000000000..afb53e0f9 --- /dev/null +++ b/src/tr1/game/output/meshes/objects.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include +#include + +void Output_Meshes_InitObjects(void); +void Output_Meshes_ShutdownObjects(void); +void Output_Meshes_ObserveLevelLoadObjects(void); +void Output_Meshes_ObserveLevelUnloadObjects(void); +void Output_Meshes_ObserveTextureAnimationObjects(void); + +void Output_Meshes_RenderObjectMesh( + const MATRIX *matrix, RGB_F tint, const OBJECT_MESH *mesh); diff --git a/src/tr1/game/output/meshes/rooms.c b/src/tr1/game/output/meshes/rooms.c new file mode 100644 index 000000000..5ebf749a4 --- /dev/null +++ b/src/tr1/game/output/meshes/rooms.c @@ -0,0 +1,468 @@ +#include "game/output/meshes/rooms.h" + +#include "game/output.h" +#include "game/output/meshes/common.h" +#include "game/output/textures.h" +#include "game/output/utils.h" +#include "game/output/vertex_range.h" +#include "game/random.h" +#include "game/room.h" + +#include +#include +#include + +#pragma pack(push, 1) +typedef struct { + // attribute 0 + XYZ_F pos; + // attribute 5 + uint16_t flags; +} M_MESH_VERTEX; + +// attribute 7 +typedef int16_t M_MESH_SHADE; +#pragma pack(pop) + +typedef struct { + int32_t vertex_start; + int32_t vertex_count; + int16_t *caustics; + VECTOR *animated_vertices; +} M_BATCH; + +static struct { + size_t vertex_count; + GLuint vao; + GLuint geom_vbo; + GLuint tex_vbo; + GLuint shade_vbo; + M_MESH_VERTEX *geom_vbo_data; + OUTPUT_MESH_TEXTURE *tex_vbo_data; + M_MESH_SHADE *shade_vbo_data; + size_t batch_count; + M_BATCH *batches; +} m_Priv; + +static int32_t m_ShadeTable[WIBBLE_SIZE] = {}; +static int32_t m_CausticsTable[WIBBLE_SIZE] = {}; +static OUTPUT_SHADER *m_Shader = nullptr; + +static M_MESH_SHADE M_ShadeCaustics(M_MESH_SHADE source, uint8_t caustic) +{ + if (Output_GetWaterEffect()) { + source += + m_ShadeTable[((uint8_t)Output_GetTime() + caustic) % WIBBLE_SIZE]; + CLAMP(source, 0, SHADE_MAX); + } else { + CLAMPG(source, SHADE_MAX); + } + return source; +} + +static M_BATCH *M_GetBatch(const ROOM *const room) +{ + // Room data gets swapped when flipping, but the VBOs do not. So a room 2 + // that gets flipped to room 17 ends up getting the data from room 2, + // whereas the VBO needs to take data from room 17. + const int16_t room_num = + Room_GetFlipStatus() && room->flipped_room != NO_ROOM_NEG + ? room->flipped_room + : Room_GetNumber(room); + return &m_Priv.batches[room_num]; +} + +static void M_FillVertex( + M_MESH_VERTEX *const out_vertex, const XYZ_16 pos, const uint16_t flags) +{ + out_vertex->pos = (XYZ_F) { .x = pos.x, .y = pos.y, .z = pos.z }; + out_vertex->flags = flags & NO_VERT_MOVE ? VERT_NO_CAUSTICS : 0; +} + +static void M_FillTexture( + OUTPUT_MESH_TEXTURE *const out_texture, const int32_t uvw_idx, + const float trapezoid_ratio_x, const float trapezoid_ratio_y) +{ + out_texture->uvw = Output_Textures_GetUVW(uvw_idx); + out_texture->texture_size = Output_Textures_GetAtlasSize(uvw_idx / 4); + out_texture->trapezoid_ratio[0] = trapezoid_ratio_x; + out_texture->trapezoid_ratio[1] = trapezoid_ratio_y; +} + +static void M_FillRoomFace4( + const ROOM *const room, M_MESH_VERTEX **out_vertex, + OUTPUT_MESH_TEXTURE **out_texture, const FACE4 *const face) +{ + for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { + const int32_t j = OUTPUT_QUAD_TO_FAN(i); + const int32_t uvw_idx = face->texture_idx * 4 + j; + const ROOM_VERTEX *const room_vertex = + &room->mesh.vertices[face->vertices[j]]; + M_FillVertex(*out_vertex, room_vertex->pos, room_vertex->flags); + M_FillTexture( + *out_texture, uvw_idx, face->texture_zw[j].z, + face->texture_zw[j].w); + (*out_vertex)++; + (*out_texture)++; + } +} + +static void M_FillRoomFace3( + const ROOM *const room, M_MESH_VERTEX **out_vertex, + OUTPUT_MESH_TEXTURE **out_texture, const FACE3 *const face) +{ + for (int32_t i = 0; i < OUTPUT_TRI_VERTICES; i++) { + const int32_t j = OUTPUT_TRI_TO_FAN(i); + const int32_t uvw_idx = face->texture_idx * 4 + j; + const ROOM_VERTEX *const room_vertex = + &room->mesh.vertices[face->vertices[j]]; + M_FillVertex(*out_vertex, room_vertex->pos, room_vertex->flags); + M_FillTexture(*out_texture, uvw_idx, 1.0f, 1.0f); + (*out_vertex)++; + (*out_texture)++; + } +} + +static void M_UpdateRoomGeometry(const ROOM *const room) +{ + M_BATCH *const batch = &m_Priv.batches[Room_GetNumber(room)]; + M_MESH_VERTEX *out_vertex = &m_Priv.geom_vbo_data[batch->vertex_start]; + OUTPUT_MESH_TEXTURE *out_texture = + &m_Priv.tex_vbo_data[batch->vertex_start]; + for (int32_t i = 0; i < room->mesh.num_face4s; i++) { + M_FillRoomFace4(room, &out_vertex, &out_texture, &room->mesh.face4s[i]); + } + for (int32_t i = 0; i < room->mesh.num_face3s; i++) { + M_FillRoomFace3(room, &out_vertex, &out_texture, &room->mesh.face3s[i]); + } +} + +static void M_UpdateRoomShades(const ROOM *const room) +{ + M_BATCH *const batch = M_GetBatch(room); + M_MESH_SHADE *out_shade = &m_Priv.shade_vbo_data[batch->vertex_start]; + for (int32_t i = 0; i < room->mesh.num_face4s; i++) { + const FACE4 *const face = &room->mesh.face4s[i]; + for (int32_t j = 0; j < OUTPUT_QUAD_VERTICES; j++) { + const int32_t k = OUTPUT_QUAD_TO_FAN(j); + *out_shade = room->mesh.vertices[face->vertices[k]].light_adder; + *out_shade = + M_ShadeCaustics(*out_shade, batch->caustics[face->vertices[k]]); + *out_shade++; + } + } + for (int32_t i = 0; i < room->mesh.num_face3s; i++) { + const FACE3 *const face = &room->mesh.face3s[i]; + for (int32_t j = 0; j < OUTPUT_TRI_VERTICES; j++) { + const int32_t k = OUTPUT_TRI_TO_FAN(j); + *out_shade = room->mesh.vertices[face->vertices[k]].light_adder; + *out_shade = + M_ShadeCaustics(*out_shade, batch->caustics[face->vertices[k]]); + out_shade++; + } + } +} + +static void M_UpdateVertices(void) +{ + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + M_UpdateRoomGeometry(room); + } + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.geom_vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, + m_Priv.vertex_count * sizeof(M_MESH_VERTEX), m_Priv.geom_vbo_data, + GL_STATIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, + m_Priv.vertex_count * sizeof(OUTPUT_MESH_TEXTURE), m_Priv.tex_vbo_data, + GL_DYNAMIC_DRAW); // allow animating textures + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.shade_vbo); + GFX_TRACK_DATA( + glBufferData, GL_ARRAY_BUFFER, + m_Priv.vertex_count * sizeof(M_MESH_SHADE), m_Priv.shade_vbo_data, + GL_DYNAMIC_DRAW); // shades are always dynamic +} + +static void M_PrepareBuffers(void) +{ + m_Priv.batch_count = Room_GetCount(); + Memory_FreePointer(&m_Priv.batches); + m_Priv.batches = Memory_Alloc(sizeof(M_BATCH) * m_Priv.batch_count); + + int32_t last_vertex = 0; + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + M_BATCH *const batch = &m_Priv.batches[i]; + batch->caustics = + Memory_Alloc(room->mesh.num_vertices * sizeof(int16_t)); + batch->vertex_start = last_vertex; + batch->vertex_count = 0; + batch->vertex_count += room->mesh.num_face4s * OUTPUT_QUAD_VERTICES; + batch->vertex_count += room->mesh.num_face3s * OUTPUT_TRI_VERTICES; + last_vertex += batch->vertex_count; + + for (int32_t j = 0; j < room->mesh.num_vertices; j++) { + batch->caustics[j] = + m_CausticsTable[(room->mesh.num_vertices - j) % WIBBLE_SIZE]; + } + } + m_Priv.vertex_count = last_vertex; + + int32_t current_vertex = 0; + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + M_BATCH *const batch = M_GetBatch(room); + batch->animated_vertices = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); + for (int32_t j = 0; j < room->mesh.num_face4s; j++) { + const FACE4 *const face = &room->mesh.face4s[j]; + if (Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + Vector_Add( + batch->animated_vertices, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = current_vertex, + .vertex_count = OUTPUT_QUAD_VERTICES, + }); + } + current_vertex += OUTPUT_QUAD_VERTICES; + } + for (int32_t j = 0; j < room->mesh.num_face3s; j++) { + const FACE3 *const face = &room->mesh.face3s[j]; + if (Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + Vector_Add( + batch->animated_vertices, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = current_vertex, + .vertex_count = OUTPUT_TRI_VERTICES, + }); + } + current_vertex += OUTPUT_TRI_VERTICES; + } + Output_GlueVertexRanges(batch->animated_vertices); + } + + m_Priv.geom_vbo_data = + Memory_Alloc(m_Priv.vertex_count * sizeof(M_MESH_VERTEX)); + m_Priv.tex_vbo_data = + Memory_Alloc(m_Priv.vertex_count * sizeof(OUTPUT_MESH_TEXTURE)); + m_Priv.shade_vbo_data = + Memory_Alloc(m_Priv.vertex_count * sizeof(M_MESH_SHADE)); + + glGenVertexArrays(1, &m_Priv.vao); + glGenBuffers(1, &m_Priv.geom_vbo); + glGenBuffers(1, &m_Priv.tex_vbo); + glGenBuffers(1, &m_Priv.shade_vbo); + + glBindVertexArray(m_Priv.vao); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.geom_vbo); + // attribute 0: position + glEnableVertexAttribArray(0); + glVertexAttribPointer( + 0, 3, GL_FLOAT, GL_FALSE, sizeof(M_MESH_VERTEX), + (void *)(intptr_t)offsetof(M_MESH_VERTEX, pos)); + + // attribute 1: normal (ignore) + + // attribute 5: flags + glEnableVertexAttribArray(5); + glVertexAttribIPointer( + 5, 1, GL_UNSIGNED_SHORT, sizeof(M_MESH_VERTEX), + (void *)(intptr_t)offsetof(M_MESH_VERTEX, flags)); + + // attribute 6: mesh color (ignore) + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + // attribute 2: uvw + glEnableVertexAttribArray(2); + glVertexAttribPointer( + 2, 3, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_TEXTURE), + (void *)(intptr_t)offsetof(OUTPUT_MESH_TEXTURE, uvw)); + + // attribute 3: texture size + glEnableVertexAttribArray(3); + glVertexAttribPointer( + 3, 4, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_TEXTURE), + (void *)(intptr_t)offsetof(OUTPUT_MESH_TEXTURE, texture_size)); + + // attribute 4: trapezoid ratios + glEnableVertexAttribArray(4); + glVertexAttribPointer( + 4, 2, GL_FLOAT, GL_FALSE, sizeof(OUTPUT_MESH_TEXTURE), + (void *)(intptr_t)offsetof(OUTPUT_MESH_TEXTURE, trapezoid_ratio)); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.shade_vbo); + + // attribute 7 (shade) + glEnableVertexAttribArray(7); + glVertexAttribPointer( + 7, 1, GL_UNSIGNED_SHORT, GL_FALSE, sizeof(M_MESH_SHADE), 0); + + M_UpdateVertices(); +} + +static void M_FreeBuffers(void) +{ + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + if (m_Priv.vao != 0) { + glDeleteVertexArrays(1, &m_Priv.vao); + m_Priv.vao = 0; + } + if (m_Priv.geom_vbo != 0) { + glDeleteBuffers(1, &m_Priv.geom_vbo); + m_Priv.geom_vbo = 0; + } + if (m_Priv.tex_vbo != 0) { + glDeleteBuffers(1, &m_Priv.tex_vbo); + m_Priv.tex_vbo = 0; + } + if (m_Priv.shade_vbo != 0) { + glDeleteBuffers(1, &m_Priv.shade_vbo); + m_Priv.shade_vbo = 0; + } + Memory_FreePointer(&m_Priv.geom_vbo_data); + Memory_FreePointer(&m_Priv.tex_vbo_data); + Memory_FreePointer(&m_Priv.shade_vbo_data); + for (int32_t i = 0; i < (int32_t)m_Priv.batch_count; i++) { + Vector_Free(m_Priv.batches[i].animated_vertices); + Memory_FreePointer(&m_Priv.batches[i].caustics); + } + Memory_FreePointer(&m_Priv.batches); +} + +void Output_Meshes_InitRooms(void) +{ + m_Shader = Output_Meshes_GetShader(); + for (int32_t i = 0; i < WIBBLE_SIZE; i++) { + const int16_t angle = (i * DEG_360) / WIBBLE_SIZE; + m_ShadeTable[i] = Math_Sin(angle) * SHADE_CAUSTICS >> W2V_SHIFT; + m_CausticsTable[i] = (Random_GetDraw() >> 5) - 0x01FF; + } +} + +void Output_Meshes_ShutdownRooms(void) +{ + M_FreeBuffers(); +} + +void Output_Meshes_ObserveLevelLoadRooms(void) +{ + M_FreeBuffers(); + M_PrepareBuffers(); +} + +static void M_FillAnimatedTextures(const ROOM *const room) +{ + const M_BATCH *const batch = M_GetBatch(room); + OUTPUT_MESH_TEXTURE *out_texture = + &m_Priv.tex_vbo_data[batch->vertex_start]; + for (int32_t j = 0; j < room->mesh.num_face4s; j++) { + const FACE4 *const face = &room->mesh.face4s[j]; + if (!Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + out_texture += OUTPUT_QUAD_VERTICES; + continue; + } + for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { + const int32_t l = OUTPUT_QUAD_TO_FAN(k); + const int32_t uvw_idx = face->texture_idx * 4 + l; + M_FillTexture( + out_texture, uvw_idx, face->texture_zw[l].z, + face->texture_zw[l].w); + out_texture++; + } + } + for (int32_t j = 0; j < room->mesh.num_face3s; j++) { + const FACE3 *const face = &room->mesh.face3s[j]; + if (!Output_Textures_IsObjectTextureAnimated(face->texture_idx)) { + out_texture += OUTPUT_TRI_VERTICES; + continue; + } + for (int32_t k = 0; k < OUTPUT_TRI_VERTICES; k++) { + const int32_t l = OUTPUT_TRI_TO_FAN(k); + const int32_t uvw_idx = face->texture_idx * 4 + l; + M_FillTexture(out_texture, uvw_idx, 1.0f, 1.0f); + out_texture++; + } + } +} + +static void M_UpdateAnimatedTextures(const ROOM *const room) +{ + const M_BATCH *const batch = M_GetBatch(room); + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.tex_vbo); + for (int32_t i = 0; i < batch->animated_vertices->count; i++) { + const OUTPUT_VERTEX_RANGE *const range = + Vector_Get(batch->animated_vertices, i); + GFX_TRACK_DATA( + glBufferSubData, GL_ARRAY_BUFFER, + range->vertex_start * sizeof(OUTPUT_MESH_TEXTURE), + range->vertex_count * sizeof(OUTPUT_MESH_TEXTURE), + &m_Priv.tex_vbo_data[range->vertex_start]); + } +} + +void Output_Meshes_ObserveTextureAnimationRooms(void) +{ + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + M_FillAnimatedTextures(room); + M_UpdateAnimatedTextures(room); + } +} + +void Output_Meshes_ObserveRoomFlip(const ROOM *const room) +{ + M_FillAnimatedTextures(room); + M_UpdateAnimatedTextures(room); +} + +void Output_Meshes_RenderRoomMesh( + const MATRIX *const matrix, const RGB_F tint, const ROOM *const room) +{ + const M_BATCH *const batch = M_GetBatch(room); + + M_UpdateRoomShades(room); + + Output_Shader_UploadMatrix(m_Shader, matrix); + Output_Shader_UploadTint(m_Shader, tint); + GFX_GL_CheckError(); + + glBindBuffer(GL_ARRAY_BUFFER, m_Priv.shade_vbo); + GFX_TRACK_SUBDATA( + glBufferSubData, GL_ARRAY_BUFFER, + batch->vertex_start * sizeof(M_MESH_SHADE), + batch->vertex_count * sizeof(M_MESH_SHADE), + &m_Priv.shade_vbo_data[batch->vertex_start]); + GFX_GL_CheckError(); + + glEnable(GL_CULL_FACE); + glBindVertexArray(m_Priv.vao); + GFX_GL_CheckError(); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, Output_Textures_GetAtlasTexture()); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, Output_Textures_GetEnvMapTexture()); + GFX_GL_CheckError(); + + if (Output_GetWibbleEffect()) { + Output_Shader_UploadWibbleEffect(m_Shader, false); + glDepthMask(GL_FALSE); + glDrawArrays(GL_TRIANGLES, batch->vertex_start, batch->vertex_count); + glDepthMask(GL_TRUE); + Output_Shader_UploadWibbleEffect(m_Shader, true); + glDrawArrays(GL_TRIANGLES, batch->vertex_start, batch->vertex_count); + } else { + Output_Shader_UploadWibbleEffect(m_Shader, false); + glDrawArrays(GL_TRIANGLES, batch->vertex_start, batch->vertex_count); + } + + glDisable(GL_CULL_FACE); + GFX_GL_CheckError(); +} diff --git a/src/tr1/game/output/meshes/rooms.h b/src/tr1/game/output/meshes/rooms.h new file mode 100644 index 000000000..0c822ac3e --- /dev/null +++ b/src/tr1/game/output/meshes/rooms.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include + +void Output_Meshes_InitRooms(void); +void Output_Meshes_ShutdownRooms(void); +void Output_Meshes_ObserveLevelLoadRooms(void); +void Output_Meshes_ObserveTextureAnimationRooms(void); +void Output_Meshes_ObserveRoomFlip(const ROOM *room); + +void Output_Meshes_RenderRoomMesh( + const MATRIX *matrix, RGB_F tint, const ROOM *room); diff --git a/src/tr1/game/output/shader.c b/src/tr1/game/output/shader.c new file mode 100644 index 000000000..947b42ca9 --- /dev/null +++ b/src/tr1/game/output/shader.c @@ -0,0 +1,187 @@ +#include "game/output/shader.h" + +#include "game/output.h" +#include "game/viewport.h" +#include "global/vars.h" + +#include +#include +#include + +typedef enum { + M_UNIFORM_TIME, + M_UNIFORM_TEX_ATLAS, + M_UNIFORM_TEX_ENV_MAP, + M_UNIFORM_SMOOTHING_ENABLED, + M_UNIFORM_ALPHA_DISCARD_ENABLED, + M_UNIFORM_ALPHA_THRESHOLD, + M_UNIFORM_TRAPEZOID_FILTER_ENABLED, + M_UNIFORM_REFLECTIONS_ENABLED, + M_UNIFORM_BRIGHTNESS_MULTIPLIER, + M_UNIFORM_GLOBAL_TINT, + M_UNIFORM_FOG, + M_UNIFORM_VIEWPORT_SIZE, + M_UNIFORM_PROJECTION_MATRIX, + M_UNIFORM_MODEL_MATRIX, + M_UNIFORM_WIBBLE_EFFECT, + M_UNIFORM_WATER_EFFECT, + M_UNIFORM_NUMBER_OF, +} M_UNIFORM; + +struct OUTPUT_SHADER { + GFX_GL_PROGRAM program; + GLint uniforms[M_UNIFORM_NUMBER_OF]; +}; + +OUTPUT_SHADER *Output_Shader_Create(const char *const path) +{ + OUTPUT_SHADER *const shader = Memory_Alloc(sizeof(OUTPUT_SHADER)); + + GFX_GL_Program_Init(&shader->program); + GFX_GL_Program_AttachShader( + &shader->program, GL_VERTEX_SHADER, path, + GFX_Context_GetConfig()->backend); + GFX_GL_Program_AttachShader( + &shader->program, GL_FRAGMENT_SHADER, path, + GFX_Context_GetConfig()->backend); + GFX_GL_Program_FragmentData(&shader->program, "outColor"); + GFX_GL_Program_Link(&shader->program); + + const char *const uniform_names[] = { + [M_UNIFORM_TIME] = "uTime", + [M_UNIFORM_TEX_ATLAS] = "uTexAtlas", + [M_UNIFORM_TEX_ENV_MAP] = "uTexEnvMap", + [M_UNIFORM_SMOOTHING_ENABLED] = "uSmoothingEnabled", + [M_UNIFORM_ALPHA_DISCARD_ENABLED] = "uAlphaDiscardEnabled", + [M_UNIFORM_ALPHA_THRESHOLD] = "uAlphaThreshold", + [M_UNIFORM_TRAPEZOID_FILTER_ENABLED] = "uTrapezoidFilterEnabled", + [M_UNIFORM_REFLECTIONS_ENABLED] = "uReflectionsEnabled", + [M_UNIFORM_BRIGHTNESS_MULTIPLIER] = "uBrightnessMultiplier", + [M_UNIFORM_GLOBAL_TINT] = "uGlobalTint", + [M_UNIFORM_FOG] = "uFog", + [M_UNIFORM_VIEWPORT_SIZE] = "uViewportSize", + [M_UNIFORM_PROJECTION_MATRIX] = "uMatProjection", + [M_UNIFORM_MODEL_MATRIX] = "uMatModelView", + [M_UNIFORM_WIBBLE_EFFECT] = "uWibbleEffect", + [M_UNIFORM_WATER_EFFECT] = "uWaterEffect", + }; + for (int32_t i = 0; i < M_UNIFORM_NUMBER_OF; i++) { + shader->uniforms[i] = + GFX_GL_Program_UniformLocation(&shader->program, uniform_names[i]); + GFX_GL_CheckError(); + } + + GFX_GL_Program_Bind(&shader->program); + glUniform1i(shader->uniforms[M_UNIFORM_TEX_ATLAS], 0); + glUniform1i(shader->uniforms[M_UNIFORM_TEX_ENV_MAP], 1); + return shader; +} + +void Output_Shader_Free(OUTPUT_SHADER *const shader) +{ + GFX_GL_Program_Close(&shader->program); + Memory_Free(shader); +} + +void Output_Shader_Bind(const OUTPUT_SHADER *const shader) +{ + GFX_GL_Program_Bind(&shader->program); +} + +void Output_Shader_UploadCommonUniforms(const OUTPUT_SHADER *const shader) +{ + GFX_GL_Program_Bind(&shader->program); + + GFX_TRACK_UNIFORM( + glUniform1f, shader->uniforms[M_UNIFORM_SMOOTHING_ENABLED], + g_Config.rendering.texture_filter); + GFX_TRACK_UNIFORM( + glUniform1f, shader->uniforms[M_UNIFORM_ALPHA_THRESHOLD], + g_Config.rendering.enable_wireframe ? -1.0f : 0.0f); + GFX_TRACK_UNIFORM( + glUniform1i, shader->uniforms[M_UNIFORM_ALPHA_DISCARD_ENABLED], + !g_Config.rendering.enable_wireframe); + GFX_TRACK_UNIFORM( + glUniform1i, shader->uniforms[M_UNIFORM_TRAPEZOID_FILTER_ENABLED], + g_Config.rendering.enable_trapezoid_filter); + GFX_TRACK_UNIFORM( + glUniform1i, shader->uniforms[M_UNIFORM_REFLECTIONS_ENABLED], + g_Config.visuals.enable_reflections); + GFX_TRACK_UNIFORM( + glUniform1f, shader->uniforms[M_UNIFORM_BRIGHTNESS_MULTIPLIER], + g_Config.visuals.brightness); + GFX_TRACK_UNIFORM( + glUniform2f, shader->uniforms[M_UNIFORM_VIEWPORT_SIZE], + GFX_Context_GetDisplayWidth(), GFX_Context_GetDisplayHeight()); + GFX_TRACK_UNIFORM( + glUniform2f, shader->uniforms[M_UNIFORM_FOG], Output_GetFogStart(), + Output_GetFogEnd()); + GFX_TRACK_UNIFORM( + glUniform1i, shader->uniforms[M_UNIFORM_TIME], Output_GetTime()); +} + +void Output_Shader_UploadMatrix( + const OUTPUT_SHADER *const shader, const MATRIX *const source) +{ + GLfloat target[4][4]; + target[0][0] = source->_00 / (float)(1 << W2V_SHIFT); + target[0][1] = source->_01 / (float)(1 << W2V_SHIFT); + target[0][2] = source->_02 / (float)(1 << W2V_SHIFT); + target[0][3] = source->_03 / (float)(1 << W2V_SHIFT); + + target[1][0] = source->_10 / (float)(1 << W2V_SHIFT); + target[1][1] = source->_11 / (float)(1 << W2V_SHIFT); + target[1][2] = source->_12 / (float)(1 << W2V_SHIFT); + target[1][3] = source->_13 / (float)(1 << W2V_SHIFT); + + target[2][0] = source->_20 / (float)(1 << W2V_SHIFT); + target[2][1] = source->_21 / (float)(1 << W2V_SHIFT); + target[2][2] = source->_22 / (float)(1 << W2V_SHIFT); + target[2][3] = source->_23 / (float)(1 << W2V_SHIFT); + + target[3][0] = 0.0; + target[3][1] = 0.0; + target[3][2] = 0.0; + target[3][3] = 1.0; + + GFX_GL_Program_Bind(&shader->program); + GFX_TRACK_UNIFORM( + glUniformMatrix4fv, shader->uniforms[M_UNIFORM_MODEL_MATRIX], 1, + GL_TRUE, &target[0][0]); +} + +void Output_Shader_UploadProjectionMatrix(const OUTPUT_SHADER *const shader) +{ + GFX_GL_Program_Bind(&shader->program); + + GLfloat projection[4][4]; + Output_GetProjectionMatrix(projection); + GFX_TRACK_UNIFORM( + glUniformMatrix4fv, shader->uniforms[M_UNIFORM_PROJECTION_MATRIX], 1, + GL_TRUE, &projection[0][0]); +} + +void Output_Shader_UploadWibbleEffect( + const OUTPUT_SHADER *const shader, const bool is_enabled) +{ + GFX_GL_Program_Bind(&shader->program); + GFX_TRACK_UNIFORM( + glUniform1i, shader->uniforms[M_UNIFORM_WIBBLE_EFFECT], is_enabled); +} + +void Output_Shader_UploadWaterEffect( + const OUTPUT_SHADER *const shader, const bool is_enabled) +{ + GFX_GL_Program_Bind(&shader->program); + GFX_TRACK_UNIFORM( + glUniform1i, shader->uniforms[M_UNIFORM_WATER_EFFECT], is_enabled); +} + +void Output_Shader_UploadTint( + const OUTPUT_SHADER *const shader, const RGB_F tint) +{ + GFX_GL_Program_Bind(&shader->program); + GFX_TRACK_UNIFORM( + glUniform3f, shader->uniforms[M_UNIFORM_GLOBAL_TINT], tint.r, tint.g, + tint.b); +} diff --git a/src/tr1/game/output/shader.h b/src/tr1/game/output/shader.h new file mode 100644 index 000000000..ea9384873 --- /dev/null +++ b/src/tr1/game/output/shader.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +typedef struct OUTPUT_SHADER OUTPUT_SHADER; + +OUTPUT_SHADER *Output_Shader_Create(const char *path); +void Output_Shader_Free(OUTPUT_SHADER *shader); +void Output_Shader_Bind(const OUTPUT_SHADER *shader); +void Output_Shader_UploadCommonUniforms(const OUTPUT_SHADER *shader); +void Output_Shader_UploadProjectionMatrix(const OUTPUT_SHADER *shader); + +// TODO: these functions are poor design. +void Output_Shader_UploadMatrix( + const OUTPUT_SHADER *shader, const MATRIX *source); +void Output_Shader_UploadWibbleEffect( + const OUTPUT_SHADER *shader, bool is_enabled); +void Output_Shader_UploadWaterEffect( + const OUTPUT_SHADER *shader, bool is_enabled); +void Output_Shader_UploadTint(const OUTPUT_SHADER *shader, RGB_F tint); diff --git a/src/tr1/game/output/sprite_program.c b/src/tr1/game/output/sprite_program.c deleted file mode 100644 index eab887c5a..000000000 --- a/src/tr1/game/output/sprite_program.c +++ /dev/null @@ -1,167 +0,0 @@ -#include "game/output/sprite_program.h" - -#include "game/output.h" -#include "game/viewport.h" -#include "global/vars.h" - -#include -#include - -typedef enum { - M_UNIFORM_TEX_ATLAS, - M_UNIFORM_TEX_UVW, - M_UNIFORM_SMOOTHING_ENABLED, - M_UNIFORM_BRIGHTNESS_MULTIPLIER, - M_UNIFORM_GLOBAL_TINT, - M_UNIFORM_VIEWPORT_CENTER, - M_UNIFORM_VIEWPORT_SIZE, - M_UNIFORM_PROJECTION_MATRIX, - M_UNIFORM_PROJECTION_MATRIX_OG, - M_UNIFORM_PHD_PERSP, - M_UNIFORM_PHD_RES_Z, - M_UNIFORM_PHD_RES_Z_BUF, - M_UNIFORM_MODEL_MATRIX, - M_UNIFORM_WIBBLE_OFFSET, - M_UNIFORM_NUMBER_OF, -} M_UNIFORM; - -static GFX_GL_PROGRAM m_Program; -static GLint m_Uniforms[M_UNIFORM_NUMBER_OF]; - -void Output_SpriteProgram_Init(void) -{ - GFX_GL_Program_Init(&m_Program); - GFX_GL_Program_AttachShader( - &m_Program, GL_VERTEX_SHADER, "shaders/sprites.glsl", - GFX_Context_GetConfig()->backend); - GFX_GL_Program_AttachShader( - &m_Program, GL_FRAGMENT_SHADER, "shaders/sprites.glsl", - GFX_Context_GetConfig()->backend); - GFX_GL_Program_FragmentData(&m_Program, "outColor"); - GFX_GL_Program_Link(&m_Program); - - const char *const uniform_names[] = { - [M_UNIFORM_TEX_ATLAS] = "uTexture", - [M_UNIFORM_TEX_UVW] = "uUVW", - [M_UNIFORM_SMOOTHING_ENABLED] = "uSmoothingEnabled", - [M_UNIFORM_BRIGHTNESS_MULTIPLIER] = "uBrightnessMultiplier", - [M_UNIFORM_GLOBAL_TINT] = "uGlobalTint", - [M_UNIFORM_VIEWPORT_CENTER] = "uViewportCenter", - [M_UNIFORM_VIEWPORT_SIZE] = "uViewportSize", - [M_UNIFORM_PROJECTION_MATRIX] = "uMatProjection", - [M_UNIFORM_PROJECTION_MATRIX_OG] = "uMatProjectionOG", - [M_UNIFORM_PHD_PERSP] = "uPhdPersp", - [M_UNIFORM_PHD_RES_Z] = "uPhdResZ", - [M_UNIFORM_PHD_RES_Z_BUF] = "uPhdResZBuf", - [M_UNIFORM_MODEL_MATRIX] = "uMatModelView", - [M_UNIFORM_WIBBLE_OFFSET] = "uWibbleOffset", - }; - for (int32_t i = 0; i < M_UNIFORM_NUMBER_OF; i++) { - m_Uniforms[i] = - GFX_GL_Program_UniformLocation(&m_Program, uniform_names[i]); - GFX_GL_CheckError(); - } - - GFX_GL_Program_Bind(&m_Program); - glUniform1i(m_Uniforms[M_UNIFORM_TEX_ATLAS], 0); - glUniform1i(m_Uniforms[M_UNIFORM_TEX_UVW], 1); -} - -void Output_SpriteProgram_Shutdown(void) -{ - GFX_GL_Program_Close(&m_Program); -} - -void Output_SpriteProgram_Bind(void) -{ - GFX_GL_Program_Bind(&m_Program); -} - -void Output_SpriteProgram_UploadCommonUniforms(void) -{ - Output_SpriteProgram_Bind(); - GFX_TRACK_UNIFORM( - glUniform1f, m_Uniforms[M_UNIFORM_SMOOTHING_ENABLED], - g_Config.rendering.texture_filter); - GFX_TRACK_UNIFORM( - glUniform1f, m_Uniforms[M_UNIFORM_BRIGHTNESS_MULTIPLIER], - g_Config.visuals.brightness); - GFX_TRACK_UNIFORM( - glUniform2f, m_Uniforms[M_UNIFORM_VIEWPORT_CENTER], - Viewport_GetCenterX(), Viewport_GetCenterY()); - GFX_TRACK_UNIFORM( - glUniform2f, m_Uniforms[M_UNIFORM_VIEWPORT_SIZE], - GFX_Context_GetDisplayWidth(), GFX_Context_GetDisplayHeight()); - GFX_TRACK_UNIFORM( - glUniform1f, m_Uniforms[M_UNIFORM_WIBBLE_OFFSET], - Output_GetWibbleOffset()); -} - -void Output_SpriteProgram_UploadMatrix(const MATRIX *const source) -{ - GLfloat target[4][4]; - target[0][0] = source->_00 / (float)(1 << W2V_SHIFT); - target[0][1] = source->_01 / (float)(1 << W2V_SHIFT); - target[0][2] = source->_02 / (float)(1 << W2V_SHIFT); - target[0][3] = source->_03 / (float)(1 << W2V_SHIFT); - - target[1][0] = source->_10 / (float)(1 << W2V_SHIFT); - target[1][1] = source->_11 / (float)(1 << W2V_SHIFT); - target[1][2] = source->_12 / (float)(1 << W2V_SHIFT); - target[1][3] = source->_13 / (float)(1 << W2V_SHIFT); - - target[2][0] = source->_20 / (float)(1 << W2V_SHIFT); - target[2][1] = source->_21 / (float)(1 << W2V_SHIFT); - target[2][2] = source->_22 / (float)(1 << W2V_SHIFT); - target[2][3] = source->_23 / (float)(1 << W2V_SHIFT); - - target[3][0] = 0.0; - target[3][1] = 0.0; - target[3][2] = 0.0; - target[3][3] = 1.0; - - Output_SpriteProgram_Bind(); - GFX_TRACK_UNIFORM( - glUniformMatrix4fv, m_Uniforms[M_UNIFORM_MODEL_MATRIX], 1, GL_TRUE, - &target[0][0]); -} - -void Output_SpriteProgram_UploadProjectionMatrix(void) -{ - Output_SpriteProgram_Bind(); - - GLfloat projection[4][4]; - Output_GetProjectionMatrix(projection); - GFX_TRACK_UNIFORM( - glUniformMatrix4fv, m_Uniforms[M_UNIFORM_PROJECTION_MATRIX], 1, GL_TRUE, - &projection[0][0]); - - const float left = 0.0f; - const float top = 0.0f; - const float right = GFX_Context_GetDisplayWidth(); - const float bottom = GFX_Context_GetDisplayHeight(); - GLfloat projection_og[4][4] = { - { 2.0f / (right - left), 0.0f, 0.0f, 0.0f }, - { 0.0f, 2.0f / (top - bottom), 0.0f, 0.0f }, - { 0.0f, 0.0f, +1.0f, 0.0f }, - { -(right + left) / (right - left), -(top + bottom) / (top - bottom), - 0.0f, 1.0f }, - }; - GFX_TRACK_UNIFORM( - glUniformMatrix4fv, m_Uniforms[M_UNIFORM_PROJECTION_MATRIX_OG], 1, - GL_FALSE, &projection_og[0][0]); - - GFX_TRACK_UNIFORM(glUniform1f, m_Uniforms[M_UNIFORM_PHD_PERSP], g_PhdPersp); - GFX_TRACK_UNIFORM( - glUniform1f, m_Uniforms[M_UNIFORM_PHD_RES_Z], - g_FltResZ / (float)(1 << W2V_SHIFT)); - GFX_TRACK_UNIFORM( - glUniform1f, m_Uniforms[M_UNIFORM_PHD_RES_Z_BUF], g_FltResZBuf); -} - -void Output_SpriteProgram_UploadTint(const RGB_F tint) -{ - GFX_GL_Program_Bind(&m_Program); - GFX_TRACK_UNIFORM( - glUniform3f, m_Uniforms[M_UNIFORM_GLOBAL_TINT], tint.r, tint.g, tint.b); -} diff --git a/src/tr1/game/output/sprite_program.h b/src/tr1/game/output/sprite_program.h deleted file mode 100644 index d56bf7bc4..000000000 --- a/src/tr1/game/output/sprite_program.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include -#include - -void Output_SpriteProgram_Init(void); -void Output_SpriteProgram_Shutdown(void); -void Output_SpriteProgram_Bind(void); -void Output_SpriteProgram_UploadCommonUniforms(void); -void Output_SpriteProgram_UploadProjectionMatrix(void); - -// TODO: these functions are poor design. -void Output_SpriteProgram_UploadMatrix(const MATRIX *source); -void Output_SpriteProgram_UploadTint(RGB_F tint); diff --git a/src/tr1/game/output/sprites.c b/src/tr1/game/output/sprites.c index f723b406e..b8c524ebd 100644 --- a/src/tr1/game/output/sprites.c +++ b/src/tr1/game/output/sprites.c @@ -1,18 +1,20 @@ #include "game/output/sprites.h" #include "game/output.h" -#include "game/output/sprite_program.h" +#include "game/output/meshes/common.h" +#include "game/output/shader.h" #include "game/output/textures.h" +#include "game/output/utils.h" +#include "game/output/vertex_range.h" #include "game/room.h" -#include "specific/s_output.h" +#include +#include #include #include #include #include -#define M_QUAD_VERTICES 6 - #pragma pack(push, 1) typedef struct { // attribute 0 @@ -24,7 +26,7 @@ typedef struct { } displacement; // attribute 2 - int32_t texture_idx; + OUTPUT_UVW uvw; } M_SPRITE_VERTEX; typedef uint16_t M_SPRITE_SHADE; @@ -34,7 +36,8 @@ typedef struct { MATRIX matrix; XYZ_32 pos; int32_t sprite_idx; - uint16_t shade; + RGB_F tint; + M_SPRITE_SHADE shade; } M_DYNAMIC_SPRITE; typedef struct { @@ -53,12 +56,13 @@ typedef struct { int32_t quad_count; } M_ROOM_BATCH; -static const int32_t m_QuadToFan[] = { 0, 1, 2, 0, 2, 3 }; +static OUTPUT_SHADER *m_Shader = nullptr; static struct { M_SPRITE_BUFFER sprite_buf; size_t room_batch_count; M_ROOM_BATCH *room_batches; + VECTOR *animated_vertices; } m_LevelData = {}; static struct { @@ -73,10 +77,8 @@ static void M_MakeQuad( for (int32_t k = 0; k < 4; k++) { out_quad[k].pos = (XYZ_F) { .x = pos.x, .y = pos.y, .z = pos.z }; - } - - for (int32_t k = 0; k < 4; k++) { - out_quad[k].texture_idx = sprite_idx * 4 + k; + out_quad[k].uvw = Output_Textures_GetUVW( + Output_Textures_GetSpritesUVWsBase() + sprite_idx * 4 + k); } out_quad[0].displacement.x = sprite->x0; @@ -118,24 +120,6 @@ static void M_BufferReallocGPU(M_SPRITE_BUFFER *const buffer) buffer->shade_vbo_data, GL_DYNAMIC_DRAW); // shades are always dynamic } -static void M_BufferUploadGPU( - M_SPRITE_BUFFER *const buffer, const int32_t start, const int32_t count) -{ - glBindBuffer(GL_ARRAY_BUFFER, buffer->geom_vbo); - GFX_GL_CheckError(); - GFX_TRACK_DATA( - glBufferSubData, GL_ARRAY_BUFFER, start * sizeof(M_SPRITE_VERTEX), - count * sizeof(M_SPRITE_VERTEX), &buffer->geom_vbo_data[start]); - GFX_GL_CheckError(); - - glBindBuffer(GL_ARRAY_BUFFER, buffer->shade_vbo); - GFX_GL_CheckError(); - GFX_TRACK_DATA( - glBufferSubData, GL_ARRAY_BUFFER, start * sizeof(M_SPRITE_SHADE), - count * sizeof(M_SPRITE_SHADE), &buffer->shade_vbo_data[start]); - GFX_GL_CheckError(); -} - static void M_PrepareBuffer( M_SPRITE_BUFFER *const buffer, const size_t capacity, const GLenum usage) { @@ -155,25 +139,47 @@ static void M_PrepareBuffer( glBindVertexArray(buffer->vao); glBindBuffer(GL_ARRAY_BUFFER, buffer->geom_vbo); + + // attribute 0: position glEnableVertexAttribArray(0); glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, sizeof(M_SPRITE_VERTEX), (void *)(intptr_t)offsetof(M_SPRITE_VERTEX, pos)); + // attribute 1: normal (becomes sprite corner displacement) glEnableVertexAttribArray(1); glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, sizeof(M_SPRITE_VERTEX), (void *)(intptr_t)offsetof(M_SPRITE_VERTEX, displacement)); + // attribute 2: uvw glEnableVertexAttribArray(2); - glVertexAttribIPointer( - 2, 1, GL_UNSIGNED_INT, sizeof(M_SPRITE_VERTEX), - (void *)(intptr_t)offsetof(M_SPRITE_VERTEX, texture_idx)); + glVertexAttribPointer( + 2, 3, GL_FLOAT, GL_FALSE, sizeof(M_SPRITE_VERTEX), + (void *)(intptr_t)offsetof(M_SPRITE_VERTEX, uvw)); + + // attribute 3: texture size + glDisableVertexAttribArray(3); + glVertexAttrib4f(3, 0.0f, 0.0f, 1.0f, 1.0f); + + // attribute 4: trapezoid ratios + glDisableVertexAttribArray(4); + glVertexAttrib2f(4, 1.0f, 1.0f); + + // attribute 5: flags + glDisableVertexAttribArray(5); + glVertexAttribI1i(5, VERT_SPRITE); + + // attribute 6: mesh color (ignore) + glDisableVertexAttribArray(6); + glVertexAttrib4f(6, 1.0f, 1.0f, 1.0f, 1.0f); glBindBuffer(GL_ARRAY_BUFFER, buffer->shade_vbo); - glEnableVertexAttribArray(3); + + // attribute 7 (shade) + glEnableVertexAttribArray(7); glVertexAttribPointer( - 3, 1, GL_UNSIGNED_SHORT, GL_FALSE, sizeof(M_SPRITE_SHADE), 0); + 7, 1, GL_UNSIGNED_SHORT, GL_FALSE, sizeof(M_SPRITE_SHADE), 0); } static void M_FreeBuffer(M_SPRITE_BUFFER *const buffer) @@ -210,15 +216,13 @@ static void M_DrawBuffer( { glBindVertexArray(buffer->vao); GFX_GL_CheckError(); - glActiveTexture(GL_TEXTURE1); - GFX_GL_CheckError(); - glBindTexture(GL_TEXTURE_BUFFER, Output_Textures_GetSpriteUVWsTexture()); - GFX_GL_CheckError(); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, Output_Textures_GetAtlasTexture()); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, Output_Textures_GetEnvMapTexture()); GFX_GL_CheckError(); - glBindTexture(GL_TEXTURE_2D_ARRAY, Output_Textures_GetAtlasTexture()); - GFX_GL_CheckError(); glDrawArrays(GL_TRIANGLES, start, count); GFX_GL_CheckError(); } @@ -236,12 +240,30 @@ static void M_PrepareLevelBatches(void) room_batch->quad_count = room->mesh.num_sprites; current_quad += room->mesh.num_sprites; } + + Vector_Clear(m_LevelData.animated_vertices); + int32_t current_vertex = 0; + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + for (int32_t j = 0; j < room->mesh.num_sprites; j++) { + const ROOM_SPRITE *const room_sprite = &room->mesh.sprites[j]; + const int16_t sprite_idx = room_sprite->texture; + if (Output_Textures_IsSpriteTextureAnimated(sprite_idx)) { + Vector_Add( + m_LevelData.animated_vertices, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = current_vertex, + .vertex_count = OUTPUT_QUAD_VERTICES, + }); + } + current_vertex += OUTPUT_QUAD_VERTICES; + } + } + Output_GlueVertexRanges(m_LevelData.animated_vertices); } -static void M_UpdateRoomGeometry(const ROOM *const room) +static void M_FillRoomGeometry(const ROOM *const room) { - M_BufferReallocGPU(&m_LevelData.sprite_buf); - int32_t current_vertex = 0; for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); @@ -254,9 +276,10 @@ static void M_UpdateRoomGeometry(const ROOM *const room) M_SPRITE_VERTEX quad[4]; M_MakeQuad(quad, sprite_idx, pos); - for (int32_t k = 0; k < M_QUAD_VERTICES; k++) { + + for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { m_LevelData.sprite_buf.geom_vbo_data[current_vertex] = - quad[m_QuadToFan[k]]; + quad[OUTPUT_QUAD_TO_FAN(k)]; current_vertex++; } } @@ -266,10 +289,10 @@ static void M_UpdateRoomGeometry(const ROOM *const room) static void M_UpdateRoomShades(const ROOM *const room) { const M_ROOM_BATCH *const room_batch = M_GetRoomBatch(room); - int32_t current_vertex = room_batch->quad_start * M_QUAD_VERTICES; + int32_t current_vertex = room_batch->quad_start * OUTPUT_QUAD_VERTICES; for (int32_t j = 0; j < room->mesh.num_sprites; j++) { const ROOM_SPRITE *const room_sprite = &room->mesh.sprites[j]; - for (int32_t k = 0; k < M_QUAD_VERTICES; k++) { + for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { m_LevelData.sprite_buf.shade_vbo_data[current_vertex] = room->mesh.vertices[room_sprite->vertex].light_adder; current_vertex++; @@ -296,25 +319,30 @@ static void M_PrepareLevelBuffers(void) } M_PrepareBuffer(&m_Dynamic.sprite_buf, 500, GL_DYNAMIC_DRAW); M_PrepareBuffer( - &m_LevelData.sprite_buf, num_quads * M_QUAD_VERTICES, GL_STATIC_DRAW); + &m_LevelData.sprite_buf, num_quads * OUTPUT_QUAD_VERTICES, + GL_DYNAMIC_DRAW); - m_LevelData.sprite_buf.vertex_count = num_quads * M_QUAD_VERTICES; + m_LevelData.sprite_buf.vertex_count = num_quads * OUTPUT_QUAD_VERTICES; for (int32_t i = 0; i < Room_GetCount(); i++) { const ROOM *const room = Room_Get(i); - M_UpdateRoomGeometry(room); + M_FillRoomGeometry(room); } + M_BufferReallocGPU(&m_LevelData.sprite_buf); } void Output_Sprites_Init(void) { - Output_SpriteProgram_Init(); + m_Shader = Output_Meshes_GetShader(); + m_LevelData.animated_vertices = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); m_Dynamic.source = Vector_CreateAtCapacity(sizeof(M_DYNAMIC_SPRITE), 50); } void Output_Sprites_Shutdown(void) { + Vector_Free(m_LevelData.animated_vertices); + Vector_Free(m_Dynamic.source); M_FreeBuffers(); - Output_SpriteProgram_Shutdown(); + m_Shader = nullptr; } void Output_Sprites_ObserveLevelLoad(void) @@ -324,8 +352,41 @@ void Output_Sprites_ObserveLevelLoad(void) M_PrepareLevelBatches(); } +void Output_Sprites_ObserveTextureAnimation(void) +{ + int32_t current_vertex = 0; + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + for (int32_t j = 0; j < room->mesh.num_sprites; j++) { + const ROOM_SPRITE *const room_sprite = &room->mesh.sprites[j]; + const int16_t sprite_idx = room_sprite->texture; + if (!Output_Textures_IsSpriteTextureAnimated(sprite_idx)) { + current_vertex += OUTPUT_QUAD_VERTICES; + continue; + } + for (int32_t k = 0; k < OUTPUT_QUAD_VERTICES; k++) { + m_LevelData.sprite_buf.geom_vbo_data[current_vertex].uvw = + Output_Textures_GetUVW( + Output_Textures_GetSpritesUVWsBase() + sprite_idx * 4 + + OUTPUT_QUAD_TO_FAN(k)); + current_vertex++; + } + } + } + glBindBuffer(GL_ARRAY_BUFFER, m_LevelData.sprite_buf.geom_vbo); + for (int32_t i = 0; i < m_LevelData.animated_vertices->count; i++) { + const OUTPUT_VERTEX_RANGE *const range = + Vector_Get(m_LevelData.animated_vertices, i); + GFX_TRACK_DATA( + glBufferSubData, GL_ARRAY_BUFFER, + range->vertex_start * sizeof(M_SPRITE_VERTEX), + range->vertex_count * sizeof(M_SPRITE_VERTEX), + &m_LevelData.sprite_buf.geom_vbo_data[range->vertex_start]); + } +} + void Output_Sprites_RenderRoomSprites( - const MATRIX *matrix, const RGB_F tint, const ROOM *const room) + const MATRIX *const matrix, const RGB_F tint, const ROOM *const room) { M_UpdateRoomShades(room); @@ -334,30 +395,32 @@ void Output_Sprites_RenderRoomSprites( GFX_GL_CheckError(); GFX_TRACK_SUBDATA( glBufferSubData, GL_ARRAY_BUFFER, - room_batch->quad_start * M_QUAD_VERTICES * sizeof(M_SPRITE_SHADE), - room_batch->quad_count * M_QUAD_VERTICES * sizeof(M_SPRITE_SHADE), + room_batch->quad_start * OUTPUT_QUAD_VERTICES * sizeof(M_SPRITE_SHADE), + room_batch->quad_count * OUTPUT_QUAD_VERTICES * sizeof(M_SPRITE_SHADE), &m_LevelData.sprite_buf - .shade_vbo_data[room_batch->quad_start * M_QUAD_VERTICES]); + .shade_vbo_data[room_batch->quad_start * OUTPUT_QUAD_VERTICES]); GFX_GL_CheckError(); - Output_SpriteProgram_UploadMatrix(matrix); - Output_SpriteProgram_UploadTint(tint); + Output_Shader_UploadWibbleEffect(m_Shader, Output_GetWibbleEffect()); + Output_Shader_UploadMatrix(m_Shader, matrix); + Output_Shader_UploadTint(m_Shader, tint); GFX_GL_CheckError(); const M_ROOM_BATCH *const batch = M_GetRoomBatch(room); M_DrawBuffer( - &m_LevelData.sprite_buf, batch->quad_start * M_QUAD_VERTICES, - batch->quad_count * M_QUAD_VERTICES); + &m_LevelData.sprite_buf, batch->quad_start * OUTPUT_QUAD_VERTICES, + batch->quad_count * OUTPUT_QUAD_VERTICES); } void Output_Sprites_RenderSingleSprite( const MATRIX *const matrix, const XYZ_32 pos, const int32_t sprite_idx, - const uint16_t shade) + const uint16_t shade, const RGB_F tint) { const M_DYNAMIC_SPRITE sprite = { .matrix = *matrix, .pos = pos, .sprite_idx = sprite_idx, + .tint = tint, .shade = shade, }; Vector_Add(m_Dynamic.source, &sprite); @@ -365,8 +428,8 @@ void Output_Sprites_RenderSingleSprite( void Output_Sprites_RenderBegin(void) { - Output_SpriteProgram_UploadCommonUniforms(); - Output_SpriteProgram_UploadProjectionMatrix(); + Output_Shader_UploadCommonUniforms(m_Shader); + Output_Shader_UploadProjectionMatrix(m_Shader); } bool Output_Sprites_Flush(void) @@ -376,10 +439,10 @@ bool Output_Sprites_Flush(void) } M_SPRITE_BUFFER *const buffer = &m_Dynamic.sprite_buf; - if ((size_t)m_Dynamic.source->count * M_QUAD_VERTICES + if ((size_t)m_Dynamic.source->count * OUTPUT_QUAD_VERTICES > buffer->vertex_capacity) { buffer->vertex_capacity = - (m_Dynamic.source->count + 50) * M_QUAD_VERTICES; + (m_Dynamic.source->count + 50) * OUTPUT_QUAD_VERTICES; buffer->geom_vbo_data = Memory_Realloc( buffer->geom_vbo_data, buffer->vertex_capacity * sizeof(M_SPRITE_VERTEX)); @@ -394,23 +457,31 @@ bool Output_Sprites_Flush(void) M_MakeQuad( quad, sprite->sprite_idx, (XYZ_16) { sprite->pos.x, sprite->pos.y, sprite->pos.z }); - for (int32_t i = 0; i < M_QUAD_VERTICES; i++) { - buffer->geom_vbo_data[buffer->vertex_count] = quad[m_QuadToFan[i]]; + for (int32_t i = 0; i < OUTPUT_QUAD_VERTICES; i++) { + buffer->geom_vbo_data[buffer->vertex_count] = + quad[OUTPUT_QUAD_TO_FAN(i)]; buffer->shade_vbo_data[buffer->vertex_count] = sprite->shade; buffer->vertex_count++; } } M_BufferReallocGPU(buffer); - Output_SpriteProgram_UploadTint((RGB_F) { 1.0f, 1.0f, 1.0f }); + Output_Shader_UploadWibbleEffect(m_Shader, Output_GetWibbleEffect()); for (int32_t i = 0; i < m_Dynamic.source->count; i++) { const M_DYNAMIC_SPRITE *const sprite = Vector_Get(m_Dynamic.source, i); - Output_SpriteProgram_UploadMatrix(&sprite->matrix); + Output_Shader_UploadTint(m_Shader, sprite->tint); + Output_Shader_UploadMatrix(m_Shader, &sprite->matrix); M_DrawBuffer( - &m_Dynamic.sprite_buf, i * M_QUAD_VERTICES, M_QUAD_VERTICES); + &m_Dynamic.sprite_buf, i * OUTPUT_QUAD_VERTICES, + OUTPUT_QUAD_VERTICES); } Vector_Clear(m_Dynamic.source); M_FlushBuffer(&m_Dynamic.sprite_buf); return true; } + +void Output_Sprites_UploadProjectionMatrix(void) +{ + Output_Shader_UploadProjectionMatrix(m_Shader); +} diff --git a/src/tr1/game/output/sprites.h b/src/tr1/game/output/sprites.h index 5549a75fd..d2838b37a 100644 --- a/src/tr1/game/output/sprites.h +++ b/src/tr1/game/output/sprites.h @@ -8,9 +8,13 @@ void Output_Sprites_Init(void); void Output_Sprites_Shutdown(void); void Output_Sprites_ObserveLevelLoad(void); +void Output_Sprites_ObserveTextureAnimation(void); +void Output_Sprites_UploadProjectionMatrix(void); + void Output_Sprites_RenderBegin(void); void Output_Sprites_RenderRoomSprites( const MATRIX *matrix, RGB_F tint, const ROOM *room); void Output_Sprites_RenderSingleSprite( - const MATRIX *matrix, XYZ_32 pos, int32_t sprite_idx, uint16_t shade); + const MATRIX *matrix, XYZ_32 pos, int32_t sprite_idx, uint16_t shade, + RGB_F tint); bool Output_Sprites_Flush(void); diff --git a/src/tr1/game/output/state.c b/src/tr1/game/output/state.c new file mode 100644 index 000000000..e0dada05f --- /dev/null +++ b/src/tr1/game/output/state.c @@ -0,0 +1,155 @@ +#include "game/output.h" +#include "game/output/meshes/common.h" +#include "game/output/meshes/objects.h" +#include "game/output/meshes/rooms.h" +#include "game/output/sprites.h" +#include "game/output/textures.h" +#include "global/vars.h" + +static int32_t m_Time = 0; +static int32_t m_AnimatedTexturesOffset = 0; + +static int32_t m_FogEnd = 0; +static RGB_F m_WaterColor = {}; + +static int32_t m_LsAdder = 0; +static int32_t m_LsDivider = 0; +static XYZ_32 m_LsVectorView = {}; + +static bool m_IsWibbleEffect = false; +static bool m_IsWaterEffect = false; +static bool m_IsShadeEffect = false; + +int32_t Output_GetFogEnd(void) +{ + return m_FogEnd; +} + +void Output_SetFogEnd(const int32_t dist) +{ + m_FogEnd = dist; + + const double near_z = Output_GetNearZ(); + const double far_z = Output_GetFarZ(); + const double res_z = 0.99 * near_z * far_z / (far_z - near_z); + g_FltResZ = res_z; + g_FltResZBuf = 0.005 + res_z / near_z; +} + +int32_t Output_GetNearZ(void) +{ + return 20 << W2V_SHIFT; +} + +int32_t Output_GetFarZ(void) +{ + return Output_GetFogEnd() << W2V_SHIFT; +} + +void Output_SetupBelowWater(const bool underwater) +{ + m_IsWaterEffect = true; + m_IsWibbleEffect = !underwater; + m_IsShadeEffect = true; + Output_RememberState(); + Output_Shader_UploadWaterEffect(Output_Meshes_GetShader(), m_IsWaterEffect); + Output_RestoreState(); +} + +void Output_SetupAboveWater(const bool underwater) +{ + m_IsWaterEffect = false; + m_IsWibbleEffect = underwater; + m_IsShadeEffect = underwater; + Output_RememberState(); + Output_Shader_UploadWaterEffect(Output_Meshes_GetShader(), m_IsWaterEffect); + Output_RestoreState(); +} + +bool Output_GetWaterEffect(void) +{ + return m_IsWaterEffect; +} + +bool Output_GetWibbleEffect(void) +{ + return m_IsWibbleEffect; +} + +void Output_SetWaterColor(const RGB_888 color) +{ + m_WaterColor.r = color.r / 255.0f; + m_WaterColor.g = color.g / 255.0f; + m_WaterColor.b = color.b / 255.0f; +} + +RGB_F Output_GetTint(void) +{ + if (m_IsShadeEffect) { + return m_WaterColor; + } + return (RGB_F) { 1.0f, 1.0f, 1.0f }; +} + +void Output_SetLightAdder(const int32_t adder) +{ + m_LsAdder = adder; +} + +int32_t Output_GetLightAdder(void) +{ + return m_LsAdder; +} + +void Output_SetLightDivider(const int32_t divider) +{ + m_LsDivider = divider; +} + +int32_t Output_GetLightDivider(void) +{ + return m_LsDivider; +} + +XYZ_32 Output_GetLightVectorView(void) +{ + return m_LsVectorView; +} + +void Output_RotateLight(const int16_t pitch, const int16_t yaw) +{ + const int32_t cp = Math_Cos(pitch); + const int32_t sp = Math_Sin(pitch); + const int32_t cy = Math_Cos(yaw); + const int32_t sy = Math_Sin(yaw); + const int32_t x = TRIGMULT2(cp, sy); + const int32_t y = -sp; + const int32_t z = TRIGMULT2(cp, cy); + const MATRIX *const m = &g_W2VMatrix; + m_LsVectorView.x = (m->_00 * x + m->_01 * y + m->_02 * z) >> W2V_SHIFT; + m_LsVectorView.y = (m->_10 * x + m->_11 * y + m->_12 * z) >> W2V_SHIFT; + m_LsVectorView.z = (m->_20 * x + m->_21 * y + m->_22 * z) >> W2V_SHIFT; +} + +int32_t Output_GetTime(void) +{ + return m_Time; +} + +void Output_AnimateTextures(const int32_t num_frames) +{ + m_Time += num_frames; + m_AnimatedTexturesOffset += num_frames; + bool update = false; + while (m_AnimatedTexturesOffset > 5) { + Output_CycleAnimatedTextures(); + update = true; + m_AnimatedTexturesOffset -= 5; + } + if (update) { + Output_Textures_CycleAnimations(); + Output_Sprites_ObserveTextureAnimation(); + Output_Meshes_ObserveTextureAnimationRooms(); + Output_Meshes_ObserveTextureAnimationObjects(); + } +} diff --git a/src/tr1/game/output/textures.c b/src/tr1/game/output/textures.c index e4acb8770..7944db03f 100644 --- a/src/tr1/game/output/textures.c +++ b/src/tr1/game/output/textures.c @@ -1,6 +1,7 @@ #include "game/output/textures.h" #include "game/output.h" +#include "game/output/vertex_range.h" #include #include @@ -9,176 +10,171 @@ #include typedef struct { - int32_t index; - int32_t count; -} M_ANIMATION_RANGE; - -typedef struct { - int32_t range_count; - M_ANIMATION_RANGE *ranges; -} M_ANIMATION_RANGES; - -typedef struct { - float u; - float v; - float w; -} M_UVW; - -typedef struct { - M_UVW corners[4]; + OUTPUT_UVW corners[4]; } M_UVW_PACK; -typedef struct { - GLuint tbo; // buffer to hold UV data - GLuint tex; // texture to hold UV buffer - int32_t count; - M_UVW_PACK *uvw; -} M_TEXTURE_DATA; - static struct { - M_ANIMATION_RANGES objects; - M_ANIMATION_RANGES sprites; + VECTOR *objects; + VECTOR *sprites; } m_AnimationRanges; static struct { - GLuint tex; // 3D texture to hold atlas pages - M_TEXTURE_DATA sprites; -} m_LevelData = {}; + GLuint tex_atlas; + GLuint tex_env_map; -int M_CompareAnimationRange(const void *const a, const void *const b) -{ - const M_ANIMATION_RANGE *const range_a = (M_ANIMATION_RANGE *)a; - const M_ANIMATION_RANGE *const range_b = (M_ANIMATION_RANGE *)b; - return range_a->index - range_b->index; -} + struct { + int32_t count; + int32_t count_objects; + int32_t count_sprites; + M_UVW_PACK *data; + M_UVW_PACK *data_objects; + M_UVW_PACK *data_sprites; -void M_MergeAndGlueAnimationRanges(M_ANIMATION_RANGES *const source) -{ - ASSERT(source != nullptr); - if (source->range_count == 0) { - return; - } + bool *animated_objects; + bool *animated_sprites; + } uvws; - // Sort ranges by index - qsort( - source->ranges, source->range_count, sizeof(M_ANIMATION_RANGE), - M_CompareAnimationRange); - - // Initialize a new index to store the merged ranges - int32_t new_range_count = 0; - - // Iterate over sorted ranges and merge them - for (int32_t i = 0; i < source->range_count; i++) { - if (new_range_count == 0) { - // First range - just copy it - source->ranges[new_range_count++] = source->ranges[i]; - } else { - // Check if the previous range can be merged with the current one - M_ANIMATION_RANGE *const last_range = - &source->ranges[new_range_count - 1]; - const int32_t last_start = last_range->index; - const int32_t last_end = last_range->index + last_range->count; - const int32_t current_start = source->ranges[i].index; - const int32_t current_end = - source->ranges[i].index + source->ranges[i].count; - - if (current_start >= last_start && current_start <= last_end) { - last_range->count = current_end - last_range->index; - } else if (current_end >= last_start && current_end <= last_end) { - last_range->index = source->ranges[i].index; - } else { - source->ranges[new_range_count++] = source->ranges[i]; - } - } - } - - // Update the range count with the new number of merged ranges - source->range_count = new_range_count; -} + struct { + OUTPUT_TEXTURE_SIZE *data; + OUTPUT_TEXTURE_SIZE *data_objects; + OUTPUT_TEXTURE_SIZE *data_sprites; + } atlas_sizes; +} m_Priv = {}; static void M_PrepareObjectAnimationRanges(void) { - m_AnimationRanges.objects.range_count = 0; + size_t required_size = 0; for (const ANIMATED_TEXTURE_RANGE *src_range = Output_GetAnimatedTextureRange(0); src_range != nullptr; src_range = src_range->next_range) { - m_AnimationRanges.objects.range_count += src_range->num_textures; + required_size += src_range->num_textures; } - m_AnimationRanges.objects.ranges = Memory_Realloc( - m_AnimationRanges.objects.ranges, - m_AnimationRanges.objects.range_count * sizeof(M_ANIMATION_RANGE)); + Vector_Clear(m_AnimationRanges.objects); + Vector_EnsureCapacity(m_AnimationRanges.objects, required_size); - M_ANIMATION_RANGE *dst_range = m_AnimationRanges.objects.ranges; for (const ANIMATED_TEXTURE_RANGE *src_range = Output_GetAnimatedTextureRange(0); src_range != nullptr; src_range = src_range->next_range) { for (int32_t i = 0; i < src_range->num_textures; i++) { - dst_range->index = src_range->textures[i]; - dst_range->count = 1; - dst_range++; + Vector_Add( + m_AnimationRanges.objects, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = src_range->textures[i], + .vertex_count = 1, + }); } } - M_MergeAndGlueAnimationRanges(&m_AnimationRanges.objects); + Output_GlueVertexRanges(m_AnimationRanges.objects); } static void M_PrepareSpriteAnimationRanges(void) { - m_AnimationRanges.sprites.range_count = 0; - for (int32_t i = 0; i < MAX_STATIC_OBJECTS; i++) { + size_t required_size = 0; + for (int32_t i = 0; i < MAX_STATIC_OBJECTS_2D; i++) { const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i); if (!obj->loaded || obj->frame_count == 1) { continue; } - m_AnimationRanges.sprites.range_count++; + required_size++; } - m_AnimationRanges.sprites.ranges = Memory_Realloc( - m_AnimationRanges.sprites.ranges, - m_AnimationRanges.sprites.range_count * sizeof(M_ANIMATION_RANGE)); + Vector_Clear(m_AnimationRanges.sprites); + Vector_EnsureCapacity(m_AnimationRanges.sprites, required_size); - M_ANIMATION_RANGE *dst_range = m_AnimationRanges.sprites.ranges; - for (int32_t i = 0; i < MAX_STATIC_OBJECTS; i++) { + for (int32_t i = 0; i < MAX_STATIC_OBJECTS_2D; i++) { const STATIC_OBJECT_2D *const obj = Object_Get2DStatic(i); if (!obj->loaded || obj->frame_count == 1) { continue; } - dst_range->index = obj->texture_idx; - dst_range->count = obj->frame_count; - dst_range++; + Vector_Add( + m_AnimationRanges.sprites, + &(OUTPUT_VERTEX_RANGE) { + .vertex_start = obj->texture_idx, + .vertex_count = obj->frame_count, + }); } - M_MergeAndGlueAnimationRanges(&m_AnimationRanges.sprites); + Output_GlueVertexRanges(m_AnimationRanges.sprites); } static void M_PrepareAnimationRanges(void) { M_PrepareObjectAnimationRanges(); M_PrepareSpriteAnimationRanges(); + + for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) { + m_Priv.uvws.animated_objects[i] = false; + for (int32_t j = 0; j < m_AnimationRanges.objects->count; j++) { + const OUTPUT_VERTEX_RANGE *const dst_range = + Vector_Get(m_AnimationRanges.objects, j); + const int32_t range_start = dst_range->vertex_start; + const int32_t range_end = range_start + dst_range->vertex_count; + if (i >= range_start && i < range_end) { + m_Priv.uvws.animated_objects[i] = true; + break; + } + } + } + + for (int32_t i = 0; i < Output_GetSpriteTextureCount(); i++) { + m_Priv.uvws.animated_sprites[i] = false; + for (int32_t j = 0; j < m_AnimationRanges.sprites->count; j++) { + const OUTPUT_VERTEX_RANGE *const dst_range = + Vector_Get(m_AnimationRanges.sprites, j); + const int32_t range_start = dst_range->vertex_start; + const int32_t range_end = range_start + dst_range->vertex_count; + if (i >= range_start && i < range_end) { + m_Priv.uvws.animated_sprites[i] = true; + break; + } + } + } } -void Output_Textures_Init(void) +static void M_FillAtlasObjectSize(const int32_t i) { + OUTPUT_TEXTURE_SIZE *const size = &m_Priv.atlas_sizes.data_objects[i]; + const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(i); + size->x0 = texture->uv[0].u; + size->y0 = texture->uv[0].v; + size->x1 = texture->uv[0].u; + size->y1 = texture->uv[0].v; + for (int32_t j = 1; j < texture->uv_count; j++) { + size->x0 = MIN(size->x0, texture->uv[j].u); + size->y0 = MIN(size->y0, texture->uv[j].v); + size->x1 = MAX(size->x1, texture->uv[j].u); + size->y1 = MAX(size->y1, texture->uv[j].v); + } + size->x0 /= 65535.0; + size->y0 /= 65535.0; + size->x1 /= 65535.0; + size->y1 /= 65535.0; } -void Output_Textures_Shutdown(void) +static void M_FillAtlasSpriteSize(const int32_t i) { - glBindTexture(GL_TEXTURE_BUFFER, 0); - glBindBuffer(GL_TEXTURE_BUFFER, 0); - if (m_LevelData.sprites.tbo != 0) { - glDeleteBuffers(1, &m_LevelData.sprites.tbo); - m_LevelData.sprites.tbo = 0; - } - if (m_LevelData.sprites.tex != 0) { - glDeleteTextures(1, &m_LevelData.sprites.tex); - m_LevelData.sprites.tex = 0; - } - if (m_LevelData.tex != 0) { - glDeleteTextures(1, &m_LevelData.tex); - m_LevelData.tex = 0; - } + OUTPUT_TEXTURE_SIZE *const size = &m_Priv.atlas_sizes.data_sprites[i]; + const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(i); + const float adj = 0.1 / 256.0f; + const float u0 = (sprite->offset & 0xFF) / 256.0f + adj; + const float v0 = (sprite->offset >> 8) / 256.0f + adj; + const float u1 = u0 + sprite->width / 65536.0f - 2 * adj; + const float v1 = v0 + sprite->height / 65536.0f - 2 * adj; + size->x0 = u0; + size->y0 = v0; + size->x1 = u1; + size->y1 = v1; +} - Memory_FreePointer(&m_AnimationRanges.objects.ranges); - Memory_FreePointer(&m_AnimationRanges.sprites.ranges); +static void M_FillObjectUVW(const int32_t i) +{ + const OBJECT_TEXTURE *const texture = Output_GetObjectTexture(i); + OUTPUT_UVW *const corners = m_Priv.uvws.data_objects[i].corners; + for (int32_t j = 0; j < 4; j++) { + corners[j].u = texture->uv[j].u / 65535.0f; + corners[j].v = texture->uv[j].v / 65535.0f; + corners[j].w = texture->tex_page; + } } static void M_FillSpriteUVW(const int32_t i) @@ -187,9 +183,9 @@ static void M_FillSpriteUVW(const int32_t i) const float adj = 0.1 / 256.0f; const float u0 = (sprite->offset & 0xFF) / 256.0f + adj; const float v0 = (sprite->offset >> 8) / 256.0f + adj; - const float u1 = u0 + (sprite->width >> 8) / 256.0f - 2 * adj; - const float v1 = v0 + (sprite->height >> 8) / 256.0f - 2 * adj; - M_UVW *corners = m_LevelData.sprites.uvw[i].corners; + const float u1 = u0 + sprite->width / 65536.0f - 2 * adj; + const float v1 = v0 + sprite->height / 65536.0f - 2 * adj; + OUTPUT_UVW *const corners = m_Priv.uvws.data_sprites[i].corners; // clang-format off corners[0].u = u0; corners[0].v = v0; corners[0].w = sprite->tex_page; corners[1].u = u1; corners[1].v = v0; corners[1].w = sprite->tex_page; @@ -198,6 +194,13 @@ static void M_FillSpriteUVW(const int32_t i) // clang-format on } +static void M_FillObjectUVWs(void) +{ + for (int32_t i = 0; i < Output_GetObjectTextureCount(); i++) { + M_FillObjectUVW(i); + } +} + static void M_FillSpriteUVWs(void) { for (int32_t i = 0; i < Output_GetSpriteTextureCount(); i++) { @@ -205,65 +208,88 @@ static void M_FillSpriteUVWs(void) } } -static void M_UploadSpriteUVWs(void) +static void M_UpdateObjectAnimatedUVWs(VECTOR *const source) { - glBindTexture(GL_TEXTURE_BUFFER, m_LevelData.sprites.tex); - GFX_TRACK_DATA( - glBufferData, GL_TEXTURE_BUFFER, - m_LevelData.sprites.count * sizeof(M_UVW_PACK), m_LevelData.sprites.uvw, - GL_DYNAMIC_DRAW); -} - -static void M_UploadSpriteAnimatedUVWs(const M_ANIMATION_RANGES *const source) -{ - glBindTexture(GL_TEXTURE_BUFFER, m_LevelData.sprites.tex); - for (int32_t i = 0; i < source->range_count; i++) { - const M_ANIMATION_RANGE *const range = &source->ranges[i]; - for (int32_t j = 0; j < range->count; j++) { - M_FillSpriteUVW(range->index + j); + for (int32_t i = 0; i < source->count; i++) { + const OUTPUT_VERTEX_RANGE *const range = Vector_Get(source, i); + for (int32_t j = 0; j < range->vertex_count; j++) { + M_FillObjectUVW(range->vertex_start + j); + M_FillAtlasObjectSize(range->vertex_start + j); } - GFX_TRACK_DATA( - glBufferSubData, GL_TEXTURE_BUFFER, - range->index * sizeof(M_UVW_PACK), - range->count * sizeof(M_UVW_PACK), - m_LevelData.sprites.uvw + range->index); } } -static void M_PrepareSpriteUVWs(void) +static void M_UpdateSpriteAnimatedUVWs(VECTOR *const source) { - m_LevelData.sprites.count = Output_GetSpriteTextureCount(); - m_LevelData.sprites.uvw = - Memory_Alloc(m_LevelData.sprites.count * sizeof(M_UVW_PACK)); + for (int32_t i = 0; i < source->count; i++) { + const OUTPUT_VERTEX_RANGE *const range = Vector_Get(source, i); + for (int32_t j = 0; j < range->vertex_count; j++) { + M_FillSpriteUVW(range->vertex_start + j); + M_FillAtlasSpriteSize(range->vertex_start + j); + } + } +} + +static void M_PrepareUVWs(void) +{ + m_Priv.uvws.count_objects = Output_GetObjectTextureCount(); + m_Priv.uvws.count_sprites = Output_GetSpriteTextureCount(); + m_Priv.uvws.count = m_Priv.uvws.count_objects + m_Priv.uvws.count_sprites; + m_Priv.uvws.data = Memory_Alloc(m_Priv.uvws.count * sizeof(M_UVW_PACK)); + m_Priv.uvws.data_objects = m_Priv.uvws.data; + m_Priv.uvws.data_sprites = m_Priv.uvws.data + m_Priv.uvws.count_objects; + m_Priv.uvws.animated_objects = + Memory_Alloc(m_Priv.uvws.count_objects * sizeof(bool)); + m_Priv.uvws.animated_sprites = + Memory_Alloc(m_Priv.uvws.count_sprites * sizeof(bool)); + M_FillObjectUVWs(); M_FillSpriteUVWs(); +} - glGenBuffers(1, &m_LevelData.sprites.tbo); - glBindBuffer(GL_TEXTURE_BUFFER, m_LevelData.sprites.tbo); +static void M_PrepareEnvMap(void) +{ + glGenTextures(1, &m_Priv.tex_env_map); + glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + GFX_GL_CheckError(); +} - GLint limit; - glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &limit); - ASSERT(m_LevelData.sprites.count * sizeof(M_UVW_PACK) <= (size_t)limit); - - glGenTextures(1, &m_LevelData.sprites.tex); - glBindTexture(GL_TEXTURE_BUFFER, m_LevelData.sprites.tex); - glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, m_LevelData.sprites.tbo); +static void M_PrepareAtlasSizes(void) +{ + const int32_t count_objects = Output_GetObjectTextureCount(); + const int32_t count_sprites = Output_GetSpriteTextureCount(); + const int32_t count = count_objects + count_sprites; + m_Priv.atlas_sizes.data = Memory_Realloc( + m_Priv.atlas_sizes.data, count * sizeof(OUTPUT_TEXTURE_SIZE)); + m_Priv.atlas_sizes.data_objects = m_Priv.atlas_sizes.data; + m_Priv.atlas_sizes.data_sprites = m_Priv.atlas_sizes.data + count_objects; + for (int32_t i = 0; i < count_objects; i++) { + M_FillAtlasObjectSize(i); + } + for (int32_t i = 0; i < count_sprites; i++) { + M_FillAtlasSpriteSize(i); + } } static void M_UploadAtlas(void) { - glGenTextures(1, &m_LevelData.tex); - glBindTexture(GL_TEXTURE_2D_ARRAY, m_LevelData.tex); + glGenTextures(1, &m_Priv.tex_atlas); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_Priv.tex_atlas); glTexStorage3D( GL_TEXTURE_2D_ARRAY, 1, // number of mipmaps GL_RGBA8, TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_HEIGHT, Output_GetTexturePageCount()); + GFX_GL_CheckError(); - // TODO: handle bilinear toggle glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + GFX_GL_CheckError(); for (int32_t i = 0; i < Output_GetTexturePageCount(); i++) { const RGBA_8888 *const input_ptr = Output_GetTexturePage32(i); @@ -278,31 +304,124 @@ static void M_UploadAtlas(void) 1, // depth GL_RGBA, GL_UNSIGNED_BYTE, input_ptr); } + GFX_GL_CheckError(); + + M_PrepareAtlasSizes(); + + GFX_GL_CheckError(); +} + +static void M_FreeLevelData(void) +{ + glBindTexture(GL_TEXTURE_BUFFER, 0); + glBindBuffer(GL_TEXTURE_BUFFER, 0); + if (m_Priv.tex_atlas != 0) { + glDeleteTextures(1, &m_Priv.tex_atlas); + m_Priv.tex_atlas = 0; + } + Memory_FreePointer(&m_Priv.uvws.data); + Memory_FreePointer(&m_Priv.uvws.animated_objects); + Memory_FreePointer(&m_Priv.uvws.animated_sprites); + Memory_FreePointer(&m_Priv.atlas_sizes.data); +} + +void Output_Textures_Init(void) +{ + M_PrepareEnvMap(); + m_AnimationRanges.objects = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); + m_AnimationRanges.sprites = Vector_Create(sizeof(OUTPUT_VERTEX_RANGE)); +} + +void Output_Textures_Shutdown(void) +{ + Vector_Free(m_AnimationRanges.objects); + Vector_Free(m_AnimationRanges.sprites); + M_FreeLevelData(); + if (m_Priv.tex_env_map != 0) { + glDeleteTextures(1, &m_Priv.tex_env_map); + m_Priv.tex_env_map = 0; + } } void Output_Textures_ObserveLevelLoad(void) { - Output_Textures_Shutdown(); + M_FreeLevelData(); + M_PrepareUVWs(); M_PrepareAnimationRanges(); - M_PrepareSpriteUVWs(); - M_UploadSpriteUVWs(); M_UploadAtlas(); } -void Output_Textures_Update(void) +void Output_Textures_UpdateEnvironmentMap(void) { - if (m_LevelData.sprites.tex == 0) { - return; - } - M_UploadSpriteAnimatedUVWs(&m_AnimationRanges.sprites); + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + GFX_GL_CheckError(); + + const GLint vp_x = viewport[0]; + const GLint vp_y = viewport[1]; + const GLint vp_w = viewport[2]; + const GLint vp_h = viewport[3]; + + const int32_t side = MIN(vp_w, vp_h); + const int32_t x = vp_x + (vp_w - side) / 2; + const int32_t y = vp_y + (vp_h - side) / 2; + const int32_t w = side; + const int32_t h = side; + + glBindTexture(GL_TEXTURE_2D, m_Priv.tex_env_map); + glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, w, h, 0); + GFX_GL_CheckError(); } -GLuint Output_Textures_GetSpriteUVWsTexture(void) +void Output_Textures_CycleAnimations(void) { - return m_LevelData.sprites.tex; + if (m_Priv.uvws.count != 0) { + M_UpdateSpriteAnimatedUVWs(m_AnimationRanges.sprites); + M_UpdateObjectAnimatedUVWs(m_AnimationRanges.objects); + } } GLuint Output_Textures_GetAtlasTexture(void) { - return m_LevelData.tex; + return m_Priv.tex_atlas; +} + +GLuint Output_Textures_GetEnvMapTexture(void) +{ + return m_Priv.tex_env_map; +} + +int32_t Output_Textures_GetSpritesUVWsBase(void) +{ + const size_t num = sizeof(M_UVW_PACK) / sizeof(OUTPUT_UVW); + ASSERT(num == 4); + return m_Priv.uvws.count_objects * num; +} + +OUTPUT_UVW Output_Textures_GetUVW(const int32_t uvw_idx) +{ + return m_Priv.uvws.data[uvw_idx / 4].corners[uvw_idx % 4]; +} + +OUTPUT_TEXTURE_SIZE Output_Textures_GetAtlasSize(const int32_t uvw_idx) +{ + return m_Priv.atlas_sizes.data[uvw_idx]; +} + +bool Output_Textures_IsObjectTextureAnimated(const int32_t texture_idx) +{ + return m_Priv.uvws.animated_objects[texture_idx]; +} + +bool Output_Textures_IsSpriteTextureAnimated(const int32_t uvw_idx) +{ + return m_Priv.uvws.animated_sprites[uvw_idx]; +} + +void Output_Textures_ApplyRenderSettings(void) +{ + // re-adjust UVs when the bilinear filter is toggled. + if (m_Priv.uvws.count != 0) { + M_FillObjectUVWs(); + } } diff --git a/src/tr1/game/output/textures.h b/src/tr1/game/output/textures.h index 1aa644bd6..3bb18c77a 100644 --- a/src/tr1/game/output/textures.h +++ b/src/tr1/game/output/textures.h @@ -2,9 +2,32 @@ #include +#pragma pack(push, 1) +typedef struct { + float x0; + float y0; + float x1; + float y1; +} OUTPUT_TEXTURE_SIZE; + +typedef struct { + float u; + float v; + float w; +} OUTPUT_UVW; +#pragma pack(pop) + void Output_Textures_Init(void); void Output_Textures_Shutdown(void); void Output_Textures_ObserveLevelLoad(void); -void Output_Textures_Update(void); -GLuint Output_Textures_GetSpriteUVWsTexture(void); +void Output_Textures_UpdateEnvironmentMap(void); +void Output_Textures_CycleAnimations(void); +void Output_Textures_ApplyRenderSettings(void); GLuint Output_Textures_GetAtlasTexture(void); +GLuint Output_Textures_GetEnvMapTexture(void); +int32_t Output_Textures_GetSpritesUVWsBase(void); + +OUTPUT_UVW Output_Textures_GetUVW(int32_t uvw_idx); +OUTPUT_TEXTURE_SIZE Output_Textures_GetAtlasSize(int32_t uvw_idx); +bool Output_Textures_IsObjectTextureAnimated(int32_t texture_idx); +bool Output_Textures_IsSpriteTextureAnimated(int32_t sprite_idx); diff --git a/src/tr1/game/output/utils.h b/src/tr1/game/output/utils.h new file mode 100644 index 000000000..790208024 --- /dev/null +++ b/src/tr1/game/output/utils.h @@ -0,0 +1,5 @@ +#define OUTPUT_QUAD_VERTICES 6 +#define OUTPUT_TRI_VERTICES 3 + +#define OUTPUT_QUAD_TO_FAN(i) ((int32_t[]) { 0, 2, 1, 0, 3, 2 }[i]) +#define OUTPUT_TRI_TO_FAN(i) ((int32_t[]) { 0, 2, 1 }[i]) diff --git a/src/tr1/game/output/vertex_range.c b/src/tr1/game/output/vertex_range.c new file mode 100644 index 000000000..f9ba9010e --- /dev/null +++ b/src/tr1/game/output/vertex_range.c @@ -0,0 +1,59 @@ +#include "game/output/vertex_range.h" + +#include + +#include + +static int M_CompareRanges(const void *const a, const void *const b) +{ + const OUTPUT_VERTEX_RANGE *const range_a = (OUTPUT_VERTEX_RANGE *)a; + const OUTPUT_VERTEX_RANGE *const range_b = (OUTPUT_VERTEX_RANGE *)b; + return range_a->vertex_start - range_b->vertex_start; +} + +void Output_GlueVertexRanges(VECTOR *const target) +{ + ASSERT(target != nullptr); + if (target->count == 0) { + return; + } + + OUTPUT_VERTEX_RANGE *const ranges = + (OUTPUT_VERTEX_RANGE *)Vector_Get(target, 0); + + qsort(ranges, target->count, sizeof(OUTPUT_VERTEX_RANGE), M_CompareRanges); + + // Initialize a new index to store the merged ranges + int32_t new_range_count = 0; + + // Iterate over sorted ranges and merge them + for (int32_t i = 0; i < target->count; i++) { + if (new_range_count == 0) { + // First range - just copy it + ranges[new_range_count] = ranges[i]; + new_range_count++; + } else { + // Check if the previous range can be merged with the current one + OUTPUT_VERTEX_RANGE *const last_range = + &ranges[new_range_count - 1]; + const int32_t last_start = last_range->vertex_start; + const int32_t last_end = + last_range->vertex_start + last_range->vertex_count; + const int32_t current_start = ranges[i].vertex_start; + const int32_t current_end = + ranges[i].vertex_start + ranges[i].vertex_count; + + if (current_start >= last_start && current_start <= last_end) { + last_range->vertex_count = + current_end - last_range->vertex_start; + } else if (current_end >= last_start && current_end <= last_end) { + last_range->vertex_start = ranges[i].vertex_start; + } else { + ranges[new_range_count++] = ranges[i]; + } + } + } + + // Update the range vertex_count with the new number of merged ranges + target->count = new_range_count; +} diff --git a/src/tr1/game/output/vertex_range.h b/src/tr1/game/output/vertex_range.h new file mode 100644 index 000000000..2788ceca1 --- /dev/null +++ b/src/tr1/game/output/vertex_range.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +typedef struct { + int32_t vertex_start; + int32_t vertex_count; +} OUTPUT_VERTEX_RANGE; + +void Output_GlueVertexRanges(VECTOR *vertex_range); diff --git a/src/tr1/game/overlay.c b/src/tr1/game/overlay.c index aa7f8b54a..97d31063f 100644 --- a/src/tr1/game/overlay.c +++ b/src/tr1/game/overlay.c @@ -7,6 +7,8 @@ #include "game/inventory.h" #include "game/items.h" #include "game/output.h" +#include "game/output/meshes/common.h" +#include "game/output/shader.h" #include "game/screen.h" #include "game/text.h" #include "game/viewport.h" @@ -15,6 +17,7 @@ #include "global/vars.h" #include +#include #include #include @@ -239,14 +242,14 @@ static void M_BarGetLocation( } if (g_GameInfo.showing_demo && bar_info->location == BL_BOTTOM_CENTER) { - *y -= M_GetBarToTextScale() * (TEXT_HEIGHT + bar_spacing); + *y -= M_GetBarToTextScale() * (TEXT_HEIGHT_FIXED + bar_spacing); } else if ( g_GameInfo.inv_ring_shown && GF_GetCurrentLevel() != nullptr && GF_GetCurrentLevel()->type == GFL_TITLE && (bar_info->location == BL_TOP_CENTER || bar_info->location == BL_BOTTOM_CENTER)) { *y = screen_margin_v + m_BarOffsetY[bar_info->location] - + M_GetBarToTextScale() * (TEXT_HEIGHT + bar_spacing); + + M_GetBarToTextScale() * (TEXT_HEIGHT_FIXED + bar_spacing); } m_BarOffsetY[bar_info->location] += *height + bar_spacing; @@ -376,6 +379,7 @@ static void M_DrawPickup3D(DISPLAY_PICKUP *pu) int16_t old_fov = Viewport_GetFOV(); Viewport_SetFOV(PICKUPS_FOV * DEG_1); Viewport_Init(vp_x1, vp_y1, vp_x2 - vp_x1, vp_y2 - vp_y1); + glViewport(vp_x1, -vp_y1, screen_width, screen_height); Output_ApplyFOV(); Matrix_PushUnit(); @@ -385,7 +389,7 @@ static void M_DrawPickup3D(DISPLAY_PICKUP *pu) Matrix_RotY(pu->rot_y); Output_SetLightDivider(0x6000); - Output_SetLightAdder(LOW_LIGHT); + Output_SetLightAdder(SHADE_LOW); Output_RotateLight(0, 0); const OBJECT *const obj = Object_Get(Inv_GetItemOption(pu->object_id)); @@ -422,6 +426,7 @@ static void M_DrawPickup3D(DISPLAY_PICKUP *pu) Matrix_Pop(); Viewport_Init(0, 0, Screen_GetResWidth(), Screen_GetResHeight()); Viewport_SetFOV(old_fov); + glViewport(0, 0, Screen_GetResWidth(), Screen_GetResHeight()); } static void M_DrawPickups3D(void) @@ -496,7 +501,7 @@ static void M_DrawPickupsSprites(void) Viewport_GetHeight() - sprite_height - sprite_height * pu->grid_y; const int32_t scale = Screen_GetRenderScaleGLRage(12288); const int16_t sprite_num = Object_Get(pu->object_id)->mesh_idx; - Output_DrawUISprite(x, y, scale, sprite_num, 4096); + Output_DrawUISprite(x, y, scale, sprite_num, SHADE_NEUTRAL); } } @@ -580,7 +585,7 @@ static void M_BarDrawEnemy(void) const OBJECT *const obj = Object_Get(g_Lara.target->object_id); m_EnemyBar.value = g_Lara.target->hit_points; m_EnemyBar.max_value = - obj->hit_points * ((g_GameInfo.bonus_flag & GBF_NGPLUS) ? 2 : 1); + obj->hit_points * (Game_IsBonusFlagSet(GBF_NGPLUS) ? 2 : 1); CLAMP(m_EnemyBar.value, 0, m_EnemyBar.max_value); Overlay_BarDraw(&m_EnemyBar, RSR_BAR); @@ -616,8 +621,7 @@ static void M_DrawAmmoInfo(void) double scale_ammo_to_bar = g_Config.ui.bar_scale / g_Config.ui.text_scale; - if (g_Lara.gun_status != LGS_READY - || (g_GameInfo.bonus_flag & GBF_NGPLUS)) { + if (g_Lara.gun_status != LGS_READY || Game_IsBonusFlagSet(GBF_NGPLUS)) { M_RemoveAmmoText(); return; } @@ -627,13 +631,14 @@ static void M_DrawAmmoInfo(void) case LGT_PISTOLS: return; case LGT_SHOTGUN: - sprintf(ammo_string, "%6d A", g_Lara.shotgun.ammo / SHOTGUN_AMMO_CLIP); + sprintf( + ammo_string, "%6d A", g_Lara.shotgun_ammo.ammo / SHOTGUN_AMMO_CLIP); break; case LGT_UZIS: - sprintf(ammo_string, "%6d C", g_Lara.uzis.ammo); + sprintf(ammo_string, "%6d C", g_Lara.uzi_ammo.ammo); break; case LGT_MAGNUMS: - sprintf(ammo_string, "%6d B", g_Lara.magnums.ammo); + sprintf(ammo_string, "%6d B", g_Lara.magnum_ammo.ammo); break; default: return; diff --git a/src/tr1/game/requester.c b/src/tr1/game/requester.c deleted file mode 100644 index 92a35168c..000000000 --- a/src/tr1/game/requester.c +++ /dev/null @@ -1,266 +0,0 @@ -#include "game/requester.h" - -#include "game/input.h" -#include "game/screen.h" -#include "game/text.h" -#include "global/const.h" -#include "global/types.h" - -#include - -#include -#include -#include - -#define BOX_BORDER 2 -#define BOX_PADDING 10 - -static void M_SetItem( - REQUEST_INFO *req, const int32_t idx, const bool is_blocked, - const char *const fmt, va_list va); - -static void M_SetItem( - REQUEST_INFO *req, const int32_t idx, const bool is_blocked, - const char *const fmt, va_list va) -{ - if (req->items[idx].content_text) { - Memory_FreePointer(&req->items[idx].content_text); - } - - va_list va_dup; - va_copy(va_dup, va); - const size_t out_size = vsnprintf(nullptr, 0, fmt, va) + 1; - req->items[idx].content_text = Memory_Alloc(sizeof(char) * out_size); - vsnprintf(req->items[idx].content_text, out_size, fmt, va_dup); - req->items[idx].is_blocked = is_blocked; - va_end(va_dup); -} - -void Requester_Init(REQUEST_INFO *req, const uint16_t max_items) -{ - req->max_items = max_items; - req->items = Memory_Alloc(sizeof(REQUESTER_ITEM) * max_items); - Requester_ClearTextstrings(req); -} - -void Requester_Shutdown(REQUEST_INFO *req) -{ - Requester_ClearTextstrings(req); - - Memory_FreePointer(&req->heading_text); - if (req->items != nullptr) { - for (int i = 0; i < req->max_items; i++) { - Memory_FreePointer(&req->items[i].content_text); - } - } - Memory_FreePointer(&req->items); -} - -void Requester_ClearTextstrings(REQUEST_INFO *req) -{ - Text_Remove(req->heading); - req->heading = nullptr; - Text_Remove(req->background); - req->background = nullptr; - Text_Remove(req->moreup); - req->moreup = nullptr; - Text_Remove(req->moredown); - req->moredown = nullptr; - - if (req->items != nullptr) { - for (int i = 0; i < req->max_items; i++) { - Text_Remove(req->items[i].content); - req->items[i].content = nullptr; - } - } - - req->items_used = 0; -} - -int32_t Requester_Display(REQUEST_INFO *req) -{ - int32_t edge_y = req->y; - int32_t lines_height = req->vis_lines * req->line_height; - int32_t box_width = req->pix_width; - int32_t box_height = - req->line_height + lines_height + BOX_PADDING * 2 + BOX_BORDER * 2; - - int32_t line_one_off = edge_y - lines_height - BOX_PADDING; - int32_t box_y = line_one_off - req->line_height - BOX_PADDING - BOX_BORDER; - int32_t line_qty = req->vis_lines; - if (req->items_used < req->vis_lines) { - line_qty = req->items_used; - } - - if (!req->background) { - req->background = Text_Create(req->x, box_y, " "); - Text_CentreH(req->background, 1); - Text_AlignBottom(req->background, 1); - Text_AddBackground( - req->background, box_width, box_height, 0, 0, TS_BACKGROUND); - Text_AddOutline(req->background, TS_BACKGROUND); - } - - if (!req->heading) { - req->heading = Text_Create( - req->x, line_one_off - req->line_height - BOX_PADDING, - req->heading_text); - Text_CentreH(req->heading, 1); - Text_AlignBottom(req->heading, 1); - Text_AddBackground( - req->heading, req->pix_width - 2 * BOX_BORDER, 0, 0, 0, TS_HEADING); - Text_AddOutline(req->heading, TS_HEADING); - } - - if (g_InputDB.menu_down) { - if (req->requested < req->items_used - 1) { - req->requested++; - } - req->line_old_offset = req->line_offset; - if (req->requested > req->line_offset + req->vis_lines - 1) { - req->line_offset++; - } - } - - if (g_InputDB.menu_up) { - if (req->requested) { - req->requested--; - } - req->line_old_offset = req->line_offset; - if (req->requested < req->line_offset) { - req->line_offset--; - } - } - - if (req->line_offset > 0) { - if (!req->moreup) { - req->moreup = Text_Create( - req->x, line_one_off - req->line_height + 2, "\\{arrow up}"); - Text_SetScale( - req->moreup, TEXT_BASE_SCALE * 2 / 3, TEXT_BASE_SCALE * 2 / 3); - Text_CentreH(req->moreup, 1); - Text_AlignBottom(req->moreup, 1); - } - } else { - Text_Remove(req->moreup); - req->moreup = nullptr; - } - - if (req->items_used > req->vis_lines + req->line_offset) { - if (!req->moredown) { - req->moredown = Text_Create(req->x, edge_y - 12, "\\{arrow down}"); - Text_SetScale( - req->moredown, TEXT_BASE_SCALE * 2 / 3, - TEXT_BASE_SCALE * 2 / 3); - Text_CentreH(req->moredown, 1); - Text_AlignBottom(req->moredown, 1); - } - } else { - Text_Remove(req->moredown); - req->moredown = nullptr; - } - - for (int32_t i = 0; i < line_qty; i++) { - REQUESTER_ITEM *const item = &req->items[i]; - - const int32_t j = req->line_offset + i; - if (j < 0 || j >= req->items_used) { - Text_RemoveBackground(item->content); - Text_RemoveOutline(item->content); - continue; - } - REQUESTER_ITEM *const row_item = &req->items[j]; - - if (item->content == nullptr && row_item->content_text != nullptr) { - item->content = Text_Create( - 0, line_one_off + req->line_height * i, row_item->content_text); - Text_CentreH(item->content, 1); - Text_AlignBottom(item->content, 1); - } - if (req->line_offset + i == req->requested) { - Text_AddBackground( - item->content, req->pix_width - BOX_PADDING - 1 * BOX_BORDER, 0, - 0, 0, TS_REQUESTED); - Text_AddOutline(item->content, TS_REQUESTED); - } else { - Text_RemoveBackground(item->content); - Text_RemoveOutline(item->content); - } - } - - if (req->line_offset != req->line_old_offset) { - for (int i = 0; i < line_qty; i++) { - REQUESTER_ITEM *const item = &req->items[i]; - if (item->content != nullptr) { - Text_ChangeText( - item->content, - req->items[req->line_offset + i].content_text); - } - } - } - - if (g_InputDB.menu_confirm) { - if (req->is_blockable && req->items[req->requested].is_blocked) { - g_Input = (INPUT_STATE) {}; - return 0; - } else { - Requester_ClearTextstrings(req); - return req->requested + 1; - } - } else if (g_InputDB.menu_back) { - Requester_ClearTextstrings(req); - return -1; - } - - return 0; -} - -void Requester_SetHeading(REQUEST_INFO *req, const char *string) -{ - Text_Remove(req->heading); - req->heading = nullptr; - if (req->heading_text) { - Memory_FreePointer(&req->heading_text); - } - - const size_t out_size = snprintf(nullptr, 0, "%s", string) + 1; - req->heading_text = Memory_Alloc(sizeof(char) * out_size); - snprintf(req->heading_text, out_size, "%s", string); -} - -void Requester_ChangeItem( - REQUEST_INFO *req, const int32_t idx, const bool is_blocked, - const char *const fmt, ...) -{ - if (idx < 0 || idx >= req->max_items || !fmt) { - return; - } - - va_list va; - va_start(va, fmt); - M_SetItem(req, idx, is_blocked, fmt, va); - va_end(va); -} - -void Requester_AddItem( - REQUEST_INFO *req, const bool is_blocked, const char *const fmt, ...) -{ - if (req->items_used >= req->max_items || !fmt) { - return; - } - - va_list va; - va_start(va, fmt); - M_SetItem(req, req->items_used, is_blocked, fmt, va); - va_end(va); - req->items_used++; -} - -void Requester_SetSize(REQUEST_INFO *req, int32_t max_lines, int16_t y) -{ - req->y = y; - req->vis_lines = Screen_GetResHeightDownscaled(RSR_TEXT) / 2 / MAX_REQLINES; - if (req->vis_lines > max_lines) { - req->vis_lines = max_lines; - } -} diff --git a/src/tr1/game/requester.h b/src/tr1/game/requester.h deleted file mode 100644 index ad0c2a30e..000000000 --- a/src/tr1/game/requester.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "global/types.h" - -#include - -void Requester_Init(REQUEST_INFO *req, uint16_t num_items); -void Requester_Shutdown(REQUEST_INFO *req); -void Requester_ClearTextstrings(REQUEST_INFO *req); -int32_t Requester_Display(REQUEST_INFO *req); -void Requester_SetHeading(REQUEST_INFO *req, const char *string); -void Requester_ChangeItem( - REQUEST_INFO *req, int32_t idx, bool is_blocked, const char *fmt, ...); -void Requester_AddItem( - REQUEST_INFO *req, bool is_blocked, const char *fmt, ...); -void Requester_SetSize(REQUEST_INFO *req, int32_t max_lines, int16_t y); diff --git a/src/tr1/game/room.c b/src/tr1/game/room.c index b80688882..696f06afd 100644 --- a/src/tr1/game/room.c +++ b/src/tr1/game/room.c @@ -26,7 +26,8 @@ static bool M_TestLava(const ITEM *const item); static void M_TriggerMusicTrack(int16_t track, const TRIGGER *const trigger) { - if (track == MX_UNUSED_0 && trigger->type == TT_ANTIPAD) { + if (track == MX_UNUSED_0 + && (trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER)) { Music_Stop(); return; } @@ -92,7 +93,7 @@ static void M_TriggerMusicTrack(int16_t track, const TRIGGER *const trigger) if (trigger->type == TT_SWITCH) { flags ^= trigger->mask; - } else if (trigger->type == TT_ANTIPAD) { + } else if (trigger->type == TT_ANTIPAD || trigger->type == TT_ANTITRIGGER) { flags &= -1 - trigger->mask; } else if (trigger->mask) { flags |= trigger->mask; @@ -220,7 +221,7 @@ SECTOR *Room_GetSector(int32_t x, int32_t y, int32_t z, int16_t *room_num) return sector; } -int16_t Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num) +int32_t Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num) { const ROOM *room = Room_Get(room_num); @@ -404,9 +405,6 @@ void Room_TestSectorTrigger(const ITEM *const item, const SECTOR *const sector) } } else { switch (trigger->type) { - case TT_TRIGGER: - break; - case TT_SWITCH: { if (!Switch_Trigger(trigger->item_index, trigger->timer)) { return; @@ -446,6 +444,9 @@ void Room_TestSectorTrigger(const ITEM *const item, const SECTOR *const sector) return; } break; + + default: + break; } } @@ -466,7 +467,9 @@ void Room_TestSectorTrigger(const ITEM *const item, const SECTOR *const sector) if (trigger->type == TT_SWITCH) { item->flags ^= trigger->mask; - } else if (trigger->type == TT_ANTIPAD) { + } else if ( + trigger->type == TT_ANTIPAD + || trigger->type == TT_ANTITRIGGER) { item->flags &= -1 - trigger->mask; } else if (trigger->mask) { item->flags |= trigger->mask; diff --git a/src/tr1/game/room.h b/src/tr1/game/room.h index d203dddfa..984a796f6 100644 --- a/src/tr1/game/room.h +++ b/src/tr1/game/room.h @@ -8,7 +8,6 @@ #include void Room_GetNewRoom(int32_t x, int32_t y, int32_t z, int16_t room_num); -int16_t Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num); void Room_TestTriggers(const ITEM *item); void Room_TestSectorTrigger(const ITEM *item, const SECTOR *sector); diff --git a/src/tr1/game/room_draw.c b/src/tr1/game/room_draw.c index f3a692c2c..be517a6a3 100644 --- a/src/tr1/game/room_draw.c +++ b/src/tr1/game/room_draw.c @@ -280,7 +280,6 @@ void Room_DrawSingleRoom(int16_t room_num) g_PhdTop = room->bound_top; g_PhdBottom = room->bound_bottom; - Output_LightRoom(room); Output_DrawRoomMesh(room); int16_t item_num = room->item_num; @@ -323,15 +322,10 @@ void Room_DrawSingleRoom(int16_t room_num) Output_DrawRoomPortals(room); } - Output_EnableScissor( - room->bound_left, room->bound_bottom, - room->bound_right - room->bound_left, - room->bound_bottom - room->bound_top); Output_RememberState(); Output_Sprites_RenderRoomSprites(g_MatrixPtr, Output_GetTint(), room); Output_Sprites_Flush(); Output_RestoreState(); - Output_DisableScissor(); Matrix_Pop(); room->bound_left = Viewport_GetMaxX(); diff --git a/src/tr1/game/savegame.h b/src/tr1/game/savegame.h index d9dfd26c9..18cbe44f9 100644 --- a/src/tr1/game/savegame.h +++ b/src/tr1/game/savegame.h @@ -1,85 +1,3 @@ #pragma once -#include "global/types.h" - #include - -#include - -// Loading a saved game is divided into two phases. First, the game reads the -// savegame file contents to look for the level number. The rest of the save -// data is stored in a special buffer in the g_GameInfo. Then the engine -// continues to execute the normal game flow and loads the specified level. -// Second phase occurs after everything finishes loading, e.g. items, -// creatures, triggers etc., and is what actually sets Lara's health, creatures -// status, triggers, inventory etc. - -#define SAVEGAME_CURRENT_VERSION 7 - -typedef enum { - VERSION_LEGACY = -1, - VERSION_0 = 0, - VERSION_1 = 1, - VERSION_2 = 2, - VERSION_3 = 3, - VERSION_4 = 4, - VERSION_5 = 5, - VERSION_6 = 6, - - VERSION_7 = 7, - // Added extra footer after the compressed BSON structure for quicker - // access to essential data, such as the level counter and the level title, - // without the need to parse the entire BSON document. - // Added TR2+ stats ammo hits/used, health packs used, distance travelled. -} SAVEGAME_VERSION; - -typedef enum { - SAVEGAME_FORMAT_LEGACY = 1, - SAVEGAME_FORMAT_BSON = 2, -} SAVEGAME_FORMAT; - -typedef struct { - SAVEGAME_FORMAT format; - char *full_path; - int32_t counter; - int32_t level_num; - char *level_title; - int16_t initial_version; - struct { - bool restart; - bool select_level; - } features; -} SAVEGAME_INFO; - -void Savegame_Init(void); -void Savegame_Shutdown(void); -bool Savegame_IsInitialised(void); - -void Savegame_InitCurrentInfo(void); - -int32_t Savegame_GetLevelNumber(int32_t slot_num); - -bool Savegame_Load(int32_t slot_num); -bool Savegame_Save(int32_t slot_num); -bool Savegame_UpdateDeathCounters(int32_t slot_num, GAME_INFO *game_info); -bool Savegame_LoadOnlyResumeInfo(int32_t slot_num, GAME_INFO *game_info); - -void Savegame_ScanSavedGames(void); -void Savegame_FillAvailableSaves(REQUEST_INFO *req); -void Savegame_FillAvailableLevels(REQUEST_INFO *req); -void Savegame_HighlightNewestSlot(void); -bool Savegame_RestartAvailable(int32_t slot_num); - -RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level); - -void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level); -void Savegame_ResetCurrentInfo(const GF_LEVEL *level); -void Savegame_CarryCurrentInfoToNextLevel( - const GF_LEVEL *src_level, const GF_LEVEL *dst_level); - -// Persist Lara's inventory to the current info. -// Used to carry over Lara's inventory between levels. -void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level); - -void Savegame_ProcessItemsBeforeLoad(void); -void Savegame_ProcessItemsBeforeSave(void); diff --git a/src/tr1/game/savegame/savegame.c b/src/tr1/game/savegame/savegame.c index d80b00b81..5dde9f215 100644 --- a/src/tr1/game/savegame/savegame.c +++ b/src/tr1/game/savegame/savegame.c @@ -3,293 +3,20 @@ #include "game/game.h" #include "game/game_flow.h" #include "game/game_string.h" -#include "game/inventory.h" -#include "game/items.h" -#include "game/lot.h" -#include "game/objects/vars.h" -#include "game/requester.h" -#include "game/room.h" -#include "game/savegame/savegame_bson.h" -#include "game/savegame/savegame_legacy.h" -#include "global/const.h" -#include "global/types.h" #include "global/vars.h" -#include #include #include -#include -#include -#include -#include -#include +#include -#include -#include -#include - -#define SAVES_DIR "saves" - -typedef struct { - bool allow_load; - bool allow_save; - SAVEGAME_FORMAT format; - const char *(*get_save_filename_pattern)(void); - bool (*fill_info)(MYFILE *fp, SAVEGAME_INFO *info); - bool (*load_from_file)(MYFILE *fp, GAME_INFO *game_info); - bool (*load_only_resume_info)(MYFILE *fp, GAME_INFO *game_info); - void (*save_to_file)(MYFILE *fp, GAME_INFO *game_info); - bool (*update_death_counters)(MYFILE *fp, GAME_INFO *game_info); -} SAVEGAME_STRATEGY; - -static int32_t m_SaveSlots = 0; -static int16_t m_NewestSlot = -1; -static SAVEGAME_INFO *m_SavegameInfo = nullptr; - -static const SAVEGAME_STRATEGY m_Strategies[] = { - { - .allow_load = true, - .allow_save = true, - .format = SAVEGAME_FORMAT_BSON, - .get_save_filename_pattern = Savegame_BSON_GetSaveFilePattern, - .fill_info = Savegame_BSON_FillInfo, - .load_from_file = Savegame_BSON_LoadFromFile, - .load_only_resume_info = Savegame_BSON_LoadOnlyResumeInfo, - .save_to_file = Savegame_BSON_SaveToFile, - .update_death_counters = Savegame_BSON_UpdateDeathCounters, - }, - { - .allow_load = true, - .allow_save = false, - .format = SAVEGAME_FORMAT_LEGACY, - .get_save_filename_pattern = Savegame_Legacy_GetSaveFilePattern, - .fill_info = Savegame_Legacy_FillInfo, - .load_from_file = Savegame_Legacy_LoadFromFile, - .load_only_resume_info = Savegame_Legacy_LoadOnlyResumeInfo, - .save_to_file = nullptr, - .update_death_counters = Savegame_Legacy_UpdateDeathCounters, - }, - { 0 }, -}; - -static void M_ClearSlots(void); -static bool M_FillSlot( - const SAVEGAME_STRATEGY *strategy, int32_t slot_num, const char *path); -static void M_ScanSavedGamesDir(const char *dir_path); -static void M_LoadPreprocess(void); -static void M_LoadPostprocess(void); - -static void M_ClearSlots(void) +int32_t Savegame_GetSlotCount(void) { - if (m_SavegameInfo == nullptr) { - return; - } - - for (int32_t i = 0; i < m_SaveSlots; i++) { - SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i]; - savegame_info->format = 0; - savegame_info->counter = -1; - savegame_info->level_num = -1; - Memory_FreePointer(&savegame_info->full_path); - Memory_FreePointer(&savegame_info->level_title); - } + return g_Config.gameplay.maximum_save_slots; } -static bool M_FillSlot( - const SAVEGAME_STRATEGY *const strategy, const int32_t slot_num, - const char *const path) +void Savegame_HighlightNewestSlot(void) { - SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num]; - if (strategy->format <= savegame_info->format) { - // do not override already filled slots or for less preferred strategies - return true; - } - - bool result = false; - MYFILE *const fp = File_Open(path, FILE_OPEN_READ); - if (fp != nullptr) { - if (strategy->fill_info(fp, savegame_info)) { - savegame_info->format = strategy->format; - Memory_FreePointer(&savegame_info->full_path); - savegame_info->full_path = Memory_DupStr(path); - result = true; - } - File_Close(fp); - } - return result; -} - -static void M_ScanSavedGamesDir(const char *const dir_path) -{ - void *dir_handle = File_OpenDirectory(dir_path); - if (dir_handle == nullptr) { - return; - } - - while (true) { - const char *file_name = File_ReadDirectory(dir_handle); - if (file_name == nullptr) { - break; - } - if (strcmp(file_name, ".") == 0 || strcmp(file_name, "..") == 0) { - continue; - } - - for (const SAVEGAME_STRATEGY *strategy = m_Strategies; strategy->format; - strategy++) { - if (!strategy->allow_load) { - continue; - } - - int32_t slot = -1; - int32_t parsed = - sscanf(file_name, strategy->get_save_filename_pattern(), &slot); - if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) { - char *file_path = String_Format("%s/%s", dir_path, file_name); - M_FillSlot(strategy, slot, file_path); - Memory_FreePointer(&file_path); - } - } - } - - File_CloseDirectory(dir_handle); -} - -static void M_LoadPreprocess(void) -{ - Savegame_InitCurrentInfo(); -} - -static void M_LoadPostprocess(void) -{ - for (int32_t i = 0; i < Item_GetLevelCount(); i++) { - ITEM *const item = Item_Get(i); - const OBJECT *const obj = Object_Get(item->object_id); - - if (obj->save_position && obj->shadow_size) { - int16_t room_num = item->room_num; - const SECTOR *const sector = Room_GetSector( - item->pos.x, item->pos.y, item->pos.z, &room_num); - item->floor = - Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); - } - - if (obj->save_flags != 0) { - item->flags &= 0xFF00; - } - - if (obj->handle_save_func != nullptr) { - obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD); - } - } - - MovableBlock_SetupFloor(); - - if (g_GameInfo.bonus_flag) { - g_Config.profile.new_game_plus_unlock = true; - } - - LOT_ClearLOT(&g_Lara.lot); -} - -void Savegame_Init(void) -{ - g_GameInfo.current = Memory_Alloc( - sizeof(RESUME_INFO) - * (GF_GetLevelTable(GFLT_MAIN)->count - + (GF_GetLevelTable(GFLT_DEMOS)->count >= 0 ? 1 : 0))); - m_SaveSlots = g_Config.gameplay.maximum_save_slots; - m_SavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots); - - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS); - for (int32_t i = 0; i < level_table->count; i++) { - RESUME_INFO *const resume_info = - Savegame_GetCurrentInfo(&level_table->levels[i]); - resume_info->flags.available = 1; - resume_info->flags.costume = 0; - resume_info->num_medis = 0; - resume_info->num_big_medis = 0; - resume_info->num_scions = 0; - resume_info->flags.got_pistols = 1; - resume_info->flags.got_shotgun = 0; - resume_info->flags.got_magnums = 0; - resume_info->flags.got_uzis = 0; - resume_info->pistol_ammo = 1000; - resume_info->shotgun_ammo = 0; - resume_info->magnum_ammo = 0; - resume_info->uzi_ammo = 0; - resume_info->gun_status = LGS_ARMLESS; - resume_info->equipped_gun_type = LGT_PISTOLS; - resume_info->holsters_gun_type = LGT_PISTOLS; - resume_info->back_gun_type = LGT_UNARMED; - resume_info->lara_hitpoints = LARA_MAX_HITPOINTS; - } -} - -RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level) -{ - ASSERT(level != nullptr); - if (GF_GetLevelTableType(level->type) == GFLT_MAIN) { - return &g_GameInfo.current[level->num]; - } else if (level->type == GFL_DEMO) { - return &g_GameInfo.current[GF_GetLevelTable(GFLT_MAIN)->count]; - } - LOG_WARNING( - "Warning: unable to get resume info for level %d (type=%s)", level->num, - ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type)); - return nullptr; -} - -void Savegame_Shutdown(void) -{ - M_ClearSlots(); - Memory_FreePointer(&m_SavegameInfo); - Memory_FreePointer(&g_GameInfo.current); -} - -bool Savegame_IsInitialised(void) -{ - return m_SavegameInfo != nullptr; -} - -void Savegame_ProcessItemsBeforeLoad(void) -{ - for (int32_t i = 0; i < Item_GetLevelCount(); i++) { - ITEM *const item = Item_Get(i); - const OBJECT *const obj = Object_Get(item->object_id); - if (obj->handle_save_func != nullptr) { - obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD); - } - } -} - -void Savegame_ProcessItemsBeforeSave(void) -{ - for (int32_t i = 0; i < Item_GetLevelCount(); i++) { - ITEM *const item = Item_Get(i); - const OBJECT *const obj = Object_Get(item->object_id); - if (obj->handle_save_func != nullptr) { - obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE); - } - } -} - -void Savegame_InitCurrentInfo(void) -{ - g_GameInfo.death_count = 0; - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); - for (int32_t i = 0; i < level_table->count; i++) { - const GF_LEVEL *const level = &level_table->levels[i]; - Savegame_ResetCurrentInfo(level); - Savegame_ApplyLogicToCurrentInfo(level); - Savegame_GetCurrentInfo(level)->flags.available = 0; - } - if (GF_GetGymLevel() != nullptr) { - Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = 1; - } - if (GF_GetFirstLevel() != nullptr) { - Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = 1; - } + g_GameInfo.select_save_slot = Savegame_GetMostRecentlyCreatedSlot(); } void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) @@ -305,17 +32,17 @@ void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) if (level == GF_GetGymLevel()) { current->flags.available = 1; current->flags.costume = 1; - current->num_medis = 0; - current->num_big_medis = 0; + current->small_medipacks = 0; + current->large_medipacks = 0; current->num_scions = 0; current->pistol_ammo = 0; current->shotgun_ammo = 0; current->magnum_ammo = 0; current->uzi_ammo = 0; - current->flags.got_pistols = 0; - current->flags.got_shotgun = 0; - current->flags.got_magnums = 0; - current->flags.got_uzis = 0; + current->flags.has_pistols = 0; + current->flags.has_shotgun = 0; + current->flags.has_magnums = 0; + current->flags.has_uzis = 0; current->equipped_gun_type = LGT_UNARMED; current->holsters_gun_type = LGT_UNARMED; current->back_gun_type = LGT_UNARMED; @@ -325,28 +52,28 @@ void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) if (level == GF_GetFirstLevel()) { current->flags.available = 1; current->flags.costume = 0; - current->num_medis = 0; - current->num_big_medis = 0; + current->small_medipacks = 0; + current->large_medipacks = 0; current->num_scions = 0; current->pistol_ammo = 1000; current->shotgun_ammo = 0; current->magnum_ammo = 0; current->uzi_ammo = 0; - current->flags.got_pistols = 1; - current->flags.got_shotgun = 0; - current->flags.got_magnums = 0; - current->flags.got_uzis = 0; + current->flags.has_pistols = 1; + current->flags.has_shotgun = 0; + current->flags.has_magnums = 0; + current->flags.has_uzis = 0; current->equipped_gun_type = LGT_PISTOLS; current->holsters_gun_type = LGT_PISTOLS; current->back_gun_type = LGT_UNARMED; current->gun_status = LGS_ARMLESS; } - if ((g_GameInfo.bonus_flag & GBF_NGPLUS) && level != GF_GetGymLevel()) { - current->flags.got_pistols = 1; - current->flags.got_shotgun = 1; - current->flags.got_magnums = 1; - current->flags.got_uzis = 1; + if (Game_IsBonusFlagSet(GBF_NGPLUS) && level != GF_GetGymLevel()) { + current->flags.has_pistols = 1; + current->flags.has_shotgun = 1; + current->flags.has_magnums = 1; + current->flags.has_uzis = 1; current->shotgun_ammo = 1234; current->magnum_ammo = 1234; current->uzi_ammo = 1234; @@ -365,11 +92,11 @@ void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) current->holsters_gun_type = current->equipped_gun_type; break; case LGT_SHOTGUN: - if (current->flags.got_pistols) { + if (current->flags.has_pistols) { current->holsters_gun_type = LGT_PISTOLS; - } else if (current->flags.got_magnums) { + } else if (current->flags.has_magnums) { current->holsters_gun_type = LGT_MAGNUMS; - } else if (current->flags.got_uzis) { + } else if (current->flags.has_uzis) { current->holsters_gun_type = LGT_UZIS; } else { current->holsters_gun_type = LGT_UNARMED; @@ -381,338 +108,10 @@ void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) } } if (current->back_gun_type == LGT_UNKNOWN) { - if (current->flags.got_shotgun) { + if (current->flags.has_shotgun) { current->back_gun_type = LGT_SHOTGUN; } else { current->back_gun_type = LGT_UNARMED; } } } - -void Savegame_ResetCurrentInfo(const GF_LEVEL *const level) -{ - LOG_INFO("Resetting resume info for level #%d", level->num); - RESUME_INFO *const current = Savegame_GetCurrentInfo(level); - memset(current, 0, sizeof(RESUME_INFO)); -} - -void Savegame_CarryCurrentInfoToNextLevel( - const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level) -{ - LOG_INFO( - "Copying resume info from level #%d to level #%d", src_level->num, - dst_level->num); - RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level); - RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level); - memcpy(dst_resume, src_resume, sizeof(RESUME_INFO)); -} - -void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level) -{ - ASSERT(level != nullptr); - RESUME_INFO *current = Savegame_GetCurrentInfo(level); - - current->lara_hitpoints = g_LaraItem->hit_points; - current->flags.available = 1; - - current->pistol_ammo = 1000; - if (Inv_RequestItem(O_PISTOL_ITEM)) { - current->flags.got_pistols = 1; - } else { - current->flags.got_pistols = 0; - } - - if (Inv_RequestItem(O_MAGNUM_ITEM)) { - current->magnum_ammo = g_Lara.magnums.ammo; - current->flags.got_magnums = 1; - } else { - current->magnum_ammo = - Inv_RequestItem(O_MAG_AMMO_ITEM) * MAGNUM_AMMO_QTY; - current->flags.got_magnums = 0; - } - - if (Inv_RequestItem(O_UZI_ITEM)) { - current->uzi_ammo = g_Lara.uzis.ammo; - current->flags.got_uzis = 1; - } else { - current->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * UZI_AMMO_QTY; - current->flags.got_uzis = 0; - } - - if (Inv_RequestItem(O_SHOTGUN_ITEM)) { - current->shotgun_ammo = g_Lara.shotgun.ammo; - current->flags.got_shotgun = 1; - } else { - current->shotgun_ammo = - Inv_RequestItem(O_SG_AMMO_ITEM) * SHOTGUN_AMMO_QTY; - current->flags.got_shotgun = 0; - } - - current->num_medis = Inv_RequestItem(O_MEDI_ITEM); - current->num_big_medis = Inv_RequestItem(O_BIGMEDI_ITEM); - current->num_scions = Inv_RequestItem(O_SCION_ITEM_1); - - current->equipped_gun_type = g_Lara.gun_type; - current->holsters_gun_type = g_Lara.holsters_gun_type; - current->back_gun_type = g_Lara.back_gun_type; - if (g_Lara.gun_status == LGS_READY) { - current->gun_status = LGS_READY; - } else { - current->gun_status = LGS_ARMLESS; - } -} - -int32_t Savegame_GetLevelNumber(const int32_t slot_num) -{ - return m_SavegameInfo[slot_num].level_num; -} - -int32_t Savegame_GetSlotCount(void) -{ - return m_SaveSlots; -} - -bool Savegame_IsSlotFree(const int32_t slot_num) -{ - return m_SavegameInfo[slot_num].level_num == -1; -} - -bool Savegame_Load(const int32_t slot_num) -{ - GAME_INFO *const game_info = &g_GameInfo; - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[slot_num]; - ASSERT(savegame_info->format != 0); - - M_LoadPreprocess(); - - bool ret = false; - const SAVEGAME_STRATEGY *strategy = m_Strategies; - while (strategy->format) { - if (savegame_info->format == strategy->format) { - MYFILE *fp = File_Open(savegame_info->full_path, FILE_OPEN_READ); - if (fp) { - ret = strategy->load_from_file(fp, game_info); - File_Close(fp); - } - break; - } - strategy++; - } - - if (ret) { - M_LoadPostprocess(); - } - - g_GameInfo.save_initial_version = m_SavegameInfo[slot_num].initial_version; - - return ret; -} - -bool Savegame_Save(const int32_t slot_num) -{ - GAME_INFO *const game_info = &g_GameInfo; - bool ret = false; - Savegame_BindSlot(slot_num); - - File_CreateDirectory(SAVES_DIR); - - const GF_LEVEL *const current_level = Game_GetCurrentLevel(); - Savegame_PersistGameToCurrentInfo(current_level); - - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); - for (int32_t i = 0; i < level_table->count; i++) { - const GF_LEVEL *const level = &level_table->levels[i]; - if (level->type == GFL_CURRENT) { - game_info->current[i] = game_info->current[current_level->num]; - } - } - const char *const level_title = Game_GetCurrentLevel()->title; - - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[slot_num]; - const bool was_slot_empty = savegame_info->full_path == nullptr; - const SAVEGAME_STRATEGY *strategy = m_Strategies; - while (strategy->format != 0) { - if (strategy->allow_save && strategy->save_to_file != nullptr) { - char *filename = - String_Format(strategy->get_save_filename_pattern(), slot_num); - char *full_path = String_Format("%s/%s", SAVES_DIR, filename); - - MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE); - if (fp != nullptr) { - g_SaveCounter++; - strategy->save_to_file(fp, game_info); - savegame_info->format = strategy->format; - Memory_FreePointer(&savegame_info->full_path); - savegame_info->full_path = Memory_DupStr(File_GetPath(fp)); - savegame_info->counter = g_SaveCounter; - savegame_info->level_num = current_level->num; - savegame_info->level_title = level_title != nullptr - ? Memory_DupStr(level_title) - : nullptr; - File_Close(fp); - ret = true; - } - - Memory_FreePointer(&filename); - Memory_FreePointer(&full_path); - } - strategy++; - } - - if (ret) { - m_NewestSlot = slot_num; - if (was_slot_empty) { - g_SavedGamesCount++; - } - Savegame_HighlightNewestSlot(); - } - - return ret; -} - -bool Savegame_UpdateDeathCounters(int32_t slot_num, GAME_INFO *game_info) -{ - ASSERT(game_info != nullptr); - ASSERT(slot_num >= 0); - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[slot_num]; - ASSERT(savegame_info->format != 0); - - bool ret = false; - const SAVEGAME_STRATEGY *strategy = &m_Strategies[0]; - while (strategy->format) { - if (savegame_info->format == strategy->format) { - MYFILE *fp = - File_Open(savegame_info->full_path, FILE_OPEN_READ_WRITE); - if (fp) { - ret = strategy->update_death_counters(fp, game_info); - File_Close(fp); - } else - break; - } - strategy++; - } - return ret; -} - -bool Savegame_LoadOnlyResumeInfo(int32_t slot_num, GAME_INFO *game_info) -{ - ASSERT(game_info != nullptr); - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[slot_num]; - ASSERT(savegame_info->format != 0); - - bool ret = false; - const SAVEGAME_STRATEGY *strategy = &m_Strategies[0]; - while (strategy->format) { - if (savegame_info->format == strategy->format) { - MYFILE *fp = File_Open(savegame_info->full_path, FILE_OPEN_READ); - if (fp) { - ret = strategy->load_only_resume_info(fp, game_info); - File_Close(fp); - } - break; - } - strategy++; - } - - g_GameInfo.save_initial_version = m_SavegameInfo[slot_num].initial_version; - - return ret; -} - -void Savegame_ScanSavedGames(void) -{ - BENCHMARK benchmark = Benchmark_Start(); - M_ClearSlots(); - - g_SaveCounter = 0; - g_SavedGamesCount = 0; - m_NewestSlot = -1; - - M_ScanSavedGamesDir(SAVES_DIR); - M_ScanSavedGamesDir("."); - - for (int32_t i = 0; i < m_SaveSlots; i++) { - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[i]; - if (savegame_info->level_title != nullptr) { - if (savegame_info->counter > g_SaveCounter) { - g_SaveCounter = savegame_info->counter; - m_NewestSlot = i; - } - g_SavedGamesCount++; - } - } - Benchmark_End(&benchmark, nullptr); -} - -void Savegame_FillAvailableSaves(REQUEST_INFO *req) -{ - Requester_ClearTextstrings(req); - Requester_Init(req, Savegame_GetSlotCount()); - - for (int i = 0; i < req->max_items; i++) { - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[i]; - - if (savegame_info->level_title != nullptr) { - Requester_AddItem( - req, false, "%s %d", savegame_info->level_title, - savegame_info->counter); - } else { - Requester_AddItem(req, true, GS(MISC_EMPTY_SLOT_FMT), i + 1); - } - } - - if (req->requested >= req->vis_lines) { - req->line_offset = req->requested - req->vis_lines + 1; - } else if (req->requested < req->line_offset) { - req->line_offset = req->requested; - } -} - -void Savegame_FillAvailableLevels(REQUEST_INFO *req) -{ - ASSERT(req != nullptr); - const int32_t slot_num = g_GameInfo.select_save_slot; - if (slot_num == -1) { - return; - } - - const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num]; - if (!savegame_info->features.select_level) { - Requester_AddItem(req, true, "%s", GS(PASSPORT_LEGACY_SELECT_LEVEL_1)); - Requester_AddItem(req, true, "%s", GS(PASSPORT_LEGACY_SELECT_LEVEL_2)); - req->requested = 0; - req->line_offset = 0; - return; - } - - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); - for (int32_t i = 0; i <= MIN(savegame_info->level_num, level_table->count); - i++) { - const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); - if (level->type != GFL_GYM) { - Requester_AddItem(req, false, "%s", level->title); - } - } - - if (g_InvMode == INV_TITLE_MODE) { - Requester_AddItem(req, false, "%s", GS(PASSPORT_STORY_SO_FAR)); - } - - req->requested = 0; - req->line_offset = 0; -} - -void Savegame_HighlightNewestSlot(void) -{ - g_SavegameRequester.requested = MAX(0, m_NewestSlot); -} - -bool Savegame_RestartAvailable(int32_t slot_num) -{ - if (slot_num == -1) { - return true; - } - - SAVEGAME_INFO *savegame_info = &m_SavegameInfo[slot_num]; - return savegame_info->features.restart; -} diff --git a/src/tr1/game/savegame/savegame_bson.c b/src/tr1/game/savegame/savegame_bson.c index 7ac63c2eb..cca545fbe 100644 --- a/src/tr1/game/savegame/savegame_bson.c +++ b/src/tr1/game/savegame/savegame_bson.c @@ -1,5 +1,3 @@ -#include "game/savegame/savegame_bson.h" - #include "game/camera.h" #include "game/carrier.h" #include "game/effects.h" @@ -11,6 +9,7 @@ #include "game/lot.h" #include "game/music.h" #include "game/room.h" +#include "game/savegame.h" #include "game/shell.h" #include "game/stats.h" #include "global/const.h" @@ -19,10 +18,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -31,40 +32,19 @@ #define SAVEGAME_BSON_MAGIC MKTAG('T', '1', 'M', 'B') -#pragma pack(push, 1) -typedef struct { - uint32_t magic; - int16_t initial_version; - uint16_t version; - int32_t compressed_size; - int32_t uncompressed_size; -} SAVEGAME_BSON_HEADER; - -typedef struct { - uint32_t flags; - int32_t counter; - int32_t level_num; - size_t title_size; -} SAVEGAME_BSON_EXTENDED_HEADER; -#pragma pack(pop) - typedef struct { int16_t count; - int16_t id_map[NUM_EFFECTS]; + int16_t id_map[MAX_EFFECTS]; } SAVEGAME_BSON_FX_ORDER; static void M_SaveRaw(MYFILE *fp, JSON_VALUE *root, int32_t version); static JSON_VALUE *M_ParseFromBuffer( const char *buffer, size_t buffer_size, int32_t *version_out); static JSON_VALUE *M_ParseFromFile(MYFILE *fp, int32_t *version_out); -static bool M_LoadResumeInfo( - JSON_ARRAY *levels_arr, RESUME_INFO *resume_info, uint16_t header_version); -static bool M_LoadDiscontinuedStartInfo( - JSON_ARRAY *start_arr, GAME_INFO *game_info); -static bool M_LoadDiscontinuedEndInfo( - JSON_ARRAY *end_arr, GAME_INFO *game_info); -static bool M_LoadMisc( - JSON_OBJECT *misc_obj, GAME_INFO *game_info, uint16_t header_version); +static bool M_LoadResumeInfo(JSON_ARRAY *levels_arr, uint16_t header_version); +static bool M_LoadDiscontinuedStartInfo(JSON_ARRAY *start_arr); +static bool M_LoadDiscontinuedEndInfo(JSON_ARRAY *end_arr); +static bool M_LoadMisc(JSON_OBJECT *misc_obj, uint16_t header_version); static bool M_LoadInventory(JSON_OBJECT *inv_obj); static bool M_LoadFlipmaps(JSON_OBJECT *flipmap_obj); static bool M_LoadCameras(JSON_ARRAY *cameras_arr); @@ -77,8 +57,8 @@ static bool M_LoadLara( JSON_OBJECT *lara_obj, LARA_INFO *lara, uint16_t header_version); static bool M_LoadCurrentMusic(JSON_OBJECT *music_obj); static bool M_LoadMusicTrackFlags(JSON_ARRAY *music_track_arr); -static JSON_ARRAY *M_DumpResumeInfo(RESUME_INFO *game_info); -static JSON_OBJECT *M_DumpMisc(GAME_INFO *game_info); +static JSON_ARRAY *M_DumpResumeInfo(void); +static JSON_OBJECT *M_DumpMisc(void); static JSON_OBJECT *M_DumpInventory(void); static JSON_OBJECT *M_DumpFlipmaps(void); static JSON_ARRAY *M_DumpCameras(void); @@ -95,6 +75,25 @@ static void M_GetFXOrder(SAVEGAME_BSON_FX_ORDER *order); static bool M_IsValidItemObject( GAME_OBJECT_ID saved_obj_id, GAME_OBJECT_ID current_obj_id); +static const char *M_GetSaveFilePattern(void); +static bool M_FillInfo(MYFILE *fp, SAVEGAME_INFO *savegame_info); +static bool M_LoadFromFile(MYFILE *fp); +static bool M_LoadOnlyResumeInfo(MYFILE *fp); +static void M_SaveToFile(MYFILE *fp, SAVEGAME_INFO *savegame_info); +static bool M_UpdateDeathCounters(MYFILE *fp, int32_t death_count); + +static SAVEGAME_STRATEGY m_Strategy = { + .allow_load = true, + .allow_save = true, + .format = SAVEGAME_FORMAT_BSON, + .get_save_file_pattern_func = M_GetSaveFilePattern, + .fill_info_func = M_FillInfo, + .load_from_file_func = M_LoadFromFile, + .load_only_resume_info_func = M_LoadOnlyResumeInfo, + .save_to_file_func = M_SaveToFile, + .update_death_counters_func = M_UpdateDeathCounters, +}; + static void M_SaveRaw(MYFILE *fp, JSON_VALUE *root, int32_t version) { size_t uncompressed_size; @@ -111,22 +110,21 @@ static void M_SaveRaw(MYFILE *fp, JSON_VALUE *root, int32_t version) Memory_FreePointer(&uncompressed); - SAVEGAME_BSON_HEADER header = { + const SAVEGAME_BSON_HEADER header = { .magic = SAVEGAME_BSON_MAGIC, - .initial_version = g_GameInfo.save_initial_version, + .initial_version = Savegame_GetInitialVersion(), .version = version, .compressed_size = compressed_size, .uncompressed_size = uncompressed_size, }; - File_WriteData(fp, &header, sizeof(header)); File_WriteData(fp, compressed, compressed_size); const GF_LEVEL *const level = Game_GetCurrentLevel(); - SAVEGAME_BSON_EXTENDED_HEADER extra_header = { - .flags = g_GameInfo.bonus_flag, - .counter = g_SaveCounter, + const SAVEGAME_BSON_EXTENDED_HEADER extra_header = { + .flags = Game_GetBonusFlag(), + .counter = Savegame_GetCounter(), .level_num = level->num, .title_size = strlen(level->title), }; @@ -139,7 +137,7 @@ static void M_SaveRaw(MYFILE *fp, JSON_VALUE *root, int32_t version) static void M_GetFXOrder(SAVEGAME_BSON_FX_ORDER *order) { order->count = 0; - for (int i = 0; i < NUM_EFFECTS; i++) { + for (int32_t i = 0; i < MAX_EFFECTS; i++) { order->id_map[i] = -1; } @@ -166,8 +164,8 @@ static bool M_IsValidItemObject( case O_PUZZLE_DONE_4: return initial_obj_id == O_PUZZLE_HOLE_4; // pickups case O_PISTOL_AMMO_ITEM: return initial_obj_id == O_PISTOL_ANIM; - case O_SG_AMMO_ITEM: return initial_obj_id == O_SHOTGUN_ITEM; - case O_MAG_AMMO_ITEM: return initial_obj_id == O_MAGNUM_ITEM; + case O_SHOTGUN_AMMO_ITEM: return initial_obj_id == O_SHOTGUN_ITEM; + case O_MAGNUM_AMMO_ITEM: return initial_obj_id == O_MAGNUM_ITEM; case O_UZI_AMMO_ITEM: return initial_obj_id == O_UZI_ITEM; // dual-state animals case O_ALLIGATOR: return initial_obj_id == O_CROCODILE; @@ -224,9 +222,8 @@ static JSON_VALUE *M_ParseFromFile(MYFILE *fp, int32_t *version_out) } static bool M_LoadResumeInfo( - JSON_ARRAY *resume_arr, RESUME_INFO *resume_info, uint16_t header_version) + JSON_ARRAY *const resume_arr, const uint16_t header_version) { - ASSERT(resume_info != nullptr); if (!resume_arr) { LOG_ERROR("Malformed save: invalid or missing resume array"); return false; @@ -243,7 +240,9 @@ static bool M_LoadResumeInfo( LOG_ERROR("Malformed save: invalid resume info"); return false; } - RESUME_INFO *resume = &resume_info[i]; + + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); resume->lara_hitpoints = JSON_ObjectGetInt( resume_obj, "lara_hitpoints", g_Config.gameplay.start_lara_hitpoints); @@ -251,8 +250,8 @@ static bool M_LoadResumeInfo( resume->magnum_ammo = JSON_ObjectGetInt(resume_obj, "magnum_ammo", 0); resume->uzi_ammo = JSON_ObjectGetInt(resume_obj, "uzi_ammo", 0); resume->shotgun_ammo = JSON_ObjectGetInt(resume_obj, "shotgun_ammo", 0); - resume->num_medis = JSON_ObjectGetInt(resume_obj, "num_medis", 0); - resume->num_big_medis = + resume->small_medipacks = JSON_ObjectGetInt(resume_obj, "num_medis", 0); + resume->large_medipacks = JSON_ObjectGetInt(resume_obj, "num_big_medis", 0); resume->num_scions = JSON_ObjectGetInt(resume_obj, "num_scions", 0); resume->gun_status = JSON_ObjectGetInt(resume_obj, "gun_status", 0); @@ -264,12 +263,12 @@ static bool M_LoadResumeInfo( JSON_ObjectGetInt(resume_obj, "back_gun_type", LGT_UNKNOWN); resume->flags.available = JSON_ObjectGetBool(resume_obj, "available", 0); - resume->flags.got_pistols = + resume->flags.has_pistols = JSON_ObjectGetBool(resume_obj, "got_pistols", 0); - resume->flags.got_magnums = + resume->flags.has_magnums = JSON_ObjectGetBool(resume_obj, "got_magnums", 0); - resume->flags.got_uzis = JSON_ObjectGetBool(resume_obj, "got_uzis", 0); - resume->flags.got_shotgun = + resume->flags.has_uzis = JSON_ObjectGetBool(resume_obj, "got_uzis", 0); + resume->flags.has_shotgun = JSON_ObjectGetBool(resume_obj, "got_shotgun", 0); resume->flags.costume = JSON_ObjectGetBool(resume_obj, "costume", 0); @@ -303,13 +302,10 @@ static bool M_LoadResumeInfo( return true; } -static bool M_LoadDiscontinuedStartInfo( - JSON_ARRAY *start_arr, GAME_INFO *game_info) +static bool M_LoadDiscontinuedStartInfo(JSON_ARRAY *const start_arr) { // This function solely exists for backward compatibility with 2.6 and 2.7 // saves. - ASSERT(game_info != nullptr); - ASSERT(game_info->current != nullptr); if (!start_arr) { LOG_ERROR( "Malformed save: invalid or missing discontinued start array"); @@ -327,7 +323,8 @@ static bool M_LoadDiscontinuedStartInfo( LOG_ERROR("Malformed save: invalid discontinued start info"); return false; } - RESUME_INFO *start = &game_info->current[i]; + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + RESUME_INFO *const start = Savegame_GetCurrentInfo(level); start->lara_hitpoints = JSON_ObjectGetInt( start_obj, "lara_hitpoints", g_Config.gameplay.start_lara_hitpoints); @@ -335,8 +332,9 @@ static bool M_LoadDiscontinuedStartInfo( start->magnum_ammo = JSON_ObjectGetInt(start_obj, "magnum_ammo", 0); start->uzi_ammo = JSON_ObjectGetInt(start_obj, "uzi_ammo", 0); start->shotgun_ammo = JSON_ObjectGetInt(start_obj, "shotgun_ammo", 0); - start->num_medis = JSON_ObjectGetInt(start_obj, "num_medis", 0); - start->num_big_medis = JSON_ObjectGetInt(start_obj, "num_big_medis", 0); + start->small_medipacks = JSON_ObjectGetInt(start_obj, "num_medis", 0); + start->large_medipacks = + JSON_ObjectGetInt(start_obj, "num_big_medis", 0); start->num_scions = JSON_ObjectGetInt(start_obj, "num_scions", 0); start->gun_status = JSON_ObjectGetInt(start_obj, "gun_status", 0); start->equipped_gun_type = @@ -344,24 +342,22 @@ static bool M_LoadDiscontinuedStartInfo( start->holsters_gun_type = LGT_UNKNOWN; start->back_gun_type = LGT_UNKNOWN; start->flags.available = JSON_ObjectGetBool(start_obj, "available", 0); - start->flags.got_pistols = + start->flags.has_pistols = JSON_ObjectGetBool(start_obj, "got_pistols", 0); - start->flags.got_magnums = + start->flags.has_magnums = JSON_ObjectGetBool(start_obj, "got_magnums", 0); - start->flags.got_uzis = JSON_ObjectGetBool(start_obj, "got_uzis", 0); - start->flags.got_shotgun = + start->flags.has_uzis = JSON_ObjectGetBool(start_obj, "got_uzis", 0); + start->flags.has_shotgun = JSON_ObjectGetBool(start_obj, "got_shotgun", 0); start->flags.costume = JSON_ObjectGetBool(start_obj, "costume", 0); } return true; } -static bool M_LoadDiscontinuedEndInfo(JSON_ARRAY *end_arr, GAME_INFO *game_info) +static bool M_LoadDiscontinuedEndInfo(JSON_ARRAY *end_arr) { // This function solely exists for backward compatibility with 2.6 and 2.7 // saves. - ASSERT(game_info != nullptr); - ASSERT(game_info->current != nullptr); if (!end_arr) { LOG_ERROR("Malformed save: invalid or missing resume info array"); return false; @@ -378,7 +374,10 @@ static bool M_LoadDiscontinuedEndInfo(JSON_ARRAY *end_arr, GAME_INFO *game_info) LOG_ERROR("Malformed save: invalid resume info"); return false; } - LEVEL_STATS *end = &game_info->current[i].stats; + + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + LEVEL_STATS *const end = &resume->stats; end->timer = JSON_ObjectGetInt(end_obj, "timer", end->timer); end->secret_flags = JSON_ObjectGetInt(end_obj, "secrets", end->secret_flags); @@ -397,18 +396,19 @@ static bool M_LoadDiscontinuedEndInfo(JSON_ARRAY *end_arr, GAME_INFO *game_info) } static bool M_LoadMisc( - JSON_OBJECT *misc_obj, GAME_INFO *game_info, uint16_t header_version) + JSON_OBJECT *const misc_obj, const uint16_t header_version) { - ASSERT(game_info != nullptr); if (!misc_obj) { LOG_ERROR("Malformed save: invalid or missing misc info"); return false; } - game_info->bonus_flag = JSON_ObjectGetInt(misc_obj, "bonus_flag", 0); + const int32_t bonus_flag = JSON_ObjectGetInt(misc_obj, "bonus_flag", 0); + Game_SetBonusFlag(bonus_flag); if (header_version >= VERSION_4) { - game_info->bonus_level_unlock = - JSON_ObjectGetBool(misc_obj, "bonus_level_unlock", 0); - game_info->death_count = JSON_ObjectGetInt(misc_obj, "death_count", -1); + const GF_LEVEL *const current_level = Game_GetCurrentLevel(); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level); + resume->stats.death_count = + JSON_ObjectGetInt(misc_obj, "death_count", -1); } return true; } @@ -677,12 +677,12 @@ static bool M_LoadEffects(JSON_ARRAY *fx_arr) return false; } - if ((signed)fx_arr->length >= NUM_EFFECTS) { + if ((signed)fx_arr->length >= MAX_EFFECTS) { LOG_WARNING( "Malformed save: expected a max of %d effect, got %d. effect over " "the " "maximum will not be created.", - NUM_EFFECTS - 1, fx_arr->length); + MAX_EFFECTS - 1, fx_arr->length); } for (int i = 0; i < (signed)fx_arr->length; i++) { @@ -873,21 +873,21 @@ static bool M_LoadLara( } if (!M_LoadAmmo( - JSON_ObjectGetObject(lara_obj, "pistols"), &lara->pistols)) { + JSON_ObjectGetObject(lara_obj, "pistols"), &lara->pistol_ammo)) { return false; } if (!M_LoadAmmo( - JSON_ObjectGetObject(lara_obj, "magnums"), &lara->magnums)) { + JSON_ObjectGetObject(lara_obj, "magnums"), &lara->magnum_ammo)) { return false; } - if (!M_LoadAmmo(JSON_ObjectGetObject(lara_obj, "uzis"), &lara->uzis)) { + if (!M_LoadAmmo(JSON_ObjectGetObject(lara_obj, "uzis"), &lara->uzi_ammo)) { return false; } if (!M_LoadAmmo( - JSON_ObjectGetObject(lara_obj, "shotgun"), &lara->shotgun)) { + JSON_ObjectGetObject(lara_obj, "shotgun"), &lara->shotgun_ammo)) { return false; } @@ -978,12 +978,12 @@ static bool M_LoadMusicTrackFlags(JSON_ARRAY *music_track_arr) return true; } -static JSON_ARRAY *M_DumpResumeInfo(RESUME_INFO *resume_info) +static JSON_ARRAY *M_DumpResumeInfo(void) { JSON_ARRAY *resume_arr = JSON_ArrayNew(); - ASSERT(resume_info != nullptr); for (int i = 0; i < GF_GetLevelTable(GFLT_MAIN)->count; i++) { - RESUME_INFO *resume = &resume_info[i]; + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); JSON_OBJECT *resume_obj = JSON_ObjectNew(); JSON_ObjectAppendInt( resume_obj, "lara_hitpoints", resume->lara_hitpoints); @@ -991,9 +991,9 @@ static JSON_ARRAY *M_DumpResumeInfo(RESUME_INFO *resume_info) JSON_ObjectAppendInt(resume_obj, "magnum_ammo", resume->magnum_ammo); JSON_ObjectAppendInt(resume_obj, "uzi_ammo", resume->uzi_ammo); JSON_ObjectAppendInt(resume_obj, "shotgun_ammo", resume->shotgun_ammo); - JSON_ObjectAppendInt(resume_obj, "num_medis", resume->num_medis); + JSON_ObjectAppendInt(resume_obj, "num_medis", resume->small_medipacks); JSON_ObjectAppendInt( - resume_obj, "num_big_medis", resume->num_big_medis); + resume_obj, "num_big_medis", resume->large_medipacks); JSON_ObjectAppendInt(resume_obj, "num_scions", resume->num_scions); JSON_ObjectAppendInt(resume_obj, "gun_status", resume->gun_status); JSON_ObjectAppendInt(resume_obj, "gun_type", resume->equipped_gun_type); @@ -1003,12 +1003,12 @@ static JSON_ARRAY *M_DumpResumeInfo(RESUME_INFO *resume_info) resume_obj, "back_gun_type", resume->back_gun_type); JSON_ObjectAppendBool(resume_obj, "available", resume->flags.available); JSON_ObjectAppendBool( - resume_obj, "got_pistols", resume->flags.got_pistols); + resume_obj, "got_pistols", resume->flags.has_pistols); JSON_ObjectAppendBool( - resume_obj, "got_magnums", resume->flags.got_magnums); - JSON_ObjectAppendBool(resume_obj, "got_uzis", resume->flags.got_uzis); + resume_obj, "got_magnums", resume->flags.has_magnums); + JSON_ObjectAppendBool(resume_obj, "got_uzis", resume->flags.has_uzis); JSON_ObjectAppendBool( - resume_obj, "got_shotgun", resume->flags.got_shotgun); + resume_obj, "got_shotgun", resume->flags.has_shotgun); JSON_ObjectAppendBool(resume_obj, "costume", resume->flags.costume); JSON_ObjectAppendInt(resume_obj, "timer", resume->stats.timer); JSON_ObjectAppendInt(resume_obj, "kills", resume->stats.kill_count); @@ -1031,14 +1031,15 @@ static JSON_ARRAY *M_DumpResumeInfo(RESUME_INFO *resume_info) return resume_arr; } -static JSON_OBJECT *M_DumpMisc(GAME_INFO *game_info) +static JSON_OBJECT *M_DumpMisc(void) { - ASSERT(game_info != nullptr); JSON_OBJECT *misc_obj = JSON_ObjectNew(); - JSON_ObjectAppendInt(misc_obj, "bonus_flag", game_info->bonus_flag); - JSON_ObjectAppendBool( - misc_obj, "bonus_level_unlock", game_info->bonus_level_unlock); - JSON_ObjectAppendInt(misc_obj, "death_count", game_info->death_count); + JSON_ObjectAppendString(misc_obj, "game_version", g_TRXVersion); + JSON_ObjectAppendInt(misc_obj, "bonus_flag", Game_GetBonusFlag()); + + const GF_LEVEL *const level = Game_GetCurrentLevel(); + const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + JSON_ObjectAppendInt(misc_obj, "death_count", resume->stats.death_count); return misc_obj; } @@ -1300,10 +1301,13 @@ static JSON_OBJECT *M_DumpLara(LARA_INFO *lara) JSON_ObjectAppendObject(lara_obj, "left_arm", M_DumpArm(&lara->left_arm)); JSON_ObjectAppendObject(lara_obj, "right_arm", M_DumpArm(&lara->right_arm)); - JSON_ObjectAppendObject(lara_obj, "pistols", M_DumpAmmo(&lara->pistols)); - JSON_ObjectAppendObject(lara_obj, "magnums", M_DumpAmmo(&lara->magnums)); - JSON_ObjectAppendObject(lara_obj, "uzis", M_DumpAmmo(&lara->uzis)); - JSON_ObjectAppendObject(lara_obj, "shotgun", M_DumpAmmo(&lara->shotgun)); + JSON_ObjectAppendObject( + lara_obj, "pistols", M_DumpAmmo(&lara->pistol_ammo)); + JSON_ObjectAppendObject( + lara_obj, "magnums", M_DumpAmmo(&lara->magnum_ammo)); + JSON_ObjectAppendObject(lara_obj, "uzis", M_DumpAmmo(&lara->uzi_ammo)); + JSON_ObjectAppendObject( + lara_obj, "shotgun", M_DumpAmmo(&lara->shotgun_ammo)); JSON_ObjectAppendObject(lara_obj, "lot", M_DumpLOT(&lara->lot)); JSON_ObjectAppendInt( @@ -1343,12 +1347,12 @@ static JSON_ARRAY *M_DumpMusicTrackFlags(void) return music_track_arr; } -const char *Savegame_BSON_GetSaveFilePattern(void) +static const char *M_GetSaveFilePattern(void) { return g_GameFlow.savegame_fmt_bson; } -bool Savegame_BSON_FillInfo(MYFILE *fp, SAVEGAME_INFO *info) +static bool M_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info) { bool ret = false; SAVEGAME_BSON_HEADER header; @@ -1389,10 +1393,8 @@ bool Savegame_BSON_FillInfo(MYFILE *fp, SAVEGAME_INFO *info) return ret; } -bool Savegame_BSON_LoadFromFile(MYFILE *fp, GAME_INFO *game_info) +static bool M_LoadFromFile(MYFILE *const fp) { - ASSERT(game_info != nullptr); - bool ret = false; int32_t version; @@ -1404,24 +1406,22 @@ bool Savegame_BSON_LoadFromFile(MYFILE *fp, GAME_INFO *game_info) } if (!M_LoadResumeInfo( - JSON_ObjectGetArray(root_obj, "current_info"), game_info->current, - version)) { + JSON_ObjectGetArray(root_obj, "current_info"), version)) { LOG_WARNING( "Failed to load RESUME_INFO current properly. " "Checking if save is legacy."); // Check for 2.6 and 2.7 legacy start and end info. if (!M_LoadDiscontinuedStartInfo( - JSON_ObjectGetArray(root_obj, "start_info"), game_info)) { + JSON_ObjectGetArray(root_obj, "start_info"))) { goto cleanup; } if (!M_LoadDiscontinuedEndInfo( - JSON_ObjectGetArray(root_obj, "end_info"), game_info)) { + JSON_ObjectGetArray(root_obj, "end_info"))) { goto cleanup; } } - if (!M_LoadMisc( - JSON_ObjectGetObject(root_obj, "misc"), game_info, version)) { + if (!M_LoadMisc(JSON_ObjectGetObject(root_obj, "misc"), version)) { goto cleanup; } @@ -1471,10 +1471,8 @@ cleanup: return ret; } -bool Savegame_BSON_LoadOnlyResumeInfo(MYFILE *fp, GAME_INFO *game_info) +static bool M_LoadOnlyResumeInfo(MYFILE *const fp) { - ASSERT(game_info != nullptr); - bool ret = false; int32_t version; @@ -1486,18 +1484,17 @@ bool Savegame_BSON_LoadOnlyResumeInfo(MYFILE *fp, GAME_INFO *game_info) } if (!M_LoadResumeInfo( - JSON_ObjectGetArray(root_obj, "current_info"), game_info->current, - version)) { + JSON_ObjectGetArray(root_obj, "current_info"), version)) { LOG_WARNING( "Failed to load RESUME_INFO current properly. Checking if " "save is legacy."); // Check for 2.6 and 2.7 legacy start and end info. if (!M_LoadDiscontinuedStartInfo( - JSON_ObjectGetArray(root_obj, "start_info"), game_info)) { + JSON_ObjectGetArray(root_obj, "start_info"))) { goto cleanup; } if (!M_LoadDiscontinuedEndInfo( - JSON_ObjectGetArray(root_obj, "end_info"), game_info)) { + JSON_ObjectGetArray(root_obj, "end_info"))) { goto cleanup; } } @@ -1509,20 +1506,17 @@ cleanup: return ret; } -void Savegame_BSON_SaveToFile(MYFILE *fp, GAME_INFO *game_info) +static void M_SaveToFile(MYFILE *const fp, SAVEGAME_INFO *const savegame_info) { - ASSERT(game_info != nullptr); - const GF_LEVEL *const current_level = Game_GetCurrentLevel(); JSON_OBJECT *root_obj = JSON_ObjectNew(); JSON_ObjectAppendString(root_obj, "level_title", current_level->title); - JSON_ObjectAppendInt(root_obj, "save_counter", g_SaveCounter); + JSON_ObjectAppendInt(root_obj, "save_counter", Savegame_GetCounter()); JSON_ObjectAppendInt(root_obj, "level_num", current_level->num); - JSON_ObjectAppendObject(root_obj, "misc", M_DumpMisc(game_info)); - JSON_ObjectAppendArray( - root_obj, "current_info", M_DumpResumeInfo(game_info->current)); + JSON_ObjectAppendObject(root_obj, "misc", M_DumpMisc()); + JSON_ObjectAppendArray(root_obj, "current_info", M_DumpResumeInfo()); JSON_ObjectAppendObject(root_obj, "inventory", M_DumpInventory()); JSON_ObjectAppendObject(root_obj, "flipmap", M_DumpFlipmaps()); JSON_ObjectAppendArray(root_obj, "cameras", M_DumpCameras()); @@ -1536,9 +1530,11 @@ void Savegame_BSON_SaveToFile(MYFILE *fp, GAME_INFO *game_info) JSON_VALUE *root = JSON_ValueFromObject(root_obj); M_SaveRaw(fp, root, SAVEGAME_CURRENT_VERSION); JSON_ValueFree(root); + + savegame_info->features.restart = true; } -bool Savegame_BSON_UpdateDeathCounters(MYFILE *fp, GAME_INFO *game_info) +static bool M_UpdateDeathCounters(MYFILE *const fp, const int32_t death_count) { bool result = false; int32_t version; @@ -1555,7 +1551,7 @@ bool Savegame_BSON_UpdateDeathCounters(MYFILE *fp, GAME_INFO *game_info) goto cleanup; } JSON_ObjectEvictKey(misc_obj, "death_count"); - JSON_ObjectAppendInt(misc_obj, "death_count", game_info->death_count); + JSON_ObjectAppendInt(misc_obj, "death_count", death_count); File_Seek(fp, 0, FILE_SEEK_SET); M_SaveRaw(fp, root, version); @@ -1565,3 +1561,5 @@ cleanup: JSON_ValueFree(root); return result; } + +REGISTER_SAVEGAME_STRATEGY(m_Strategy) diff --git a/src/tr1/game/savegame/savegame_bson.h b/src/tr1/game/savegame/savegame_bson.h deleted file mode 100644 index 9e3d93118..000000000 --- a/src/tr1/game/savegame/savegame_bson.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "game/savegame.h" -#include "global/types.h" - -#include - -#include - -// TR1X implementation of savegames. - -const char *Savegame_BSON_GetSaveFilePattern(void); -bool Savegame_BSON_FillInfo(MYFILE *fp, SAVEGAME_INFO *info); -bool Savegame_BSON_LoadFromFile(MYFILE *fp, GAME_INFO *game_info); -bool Savegame_BSON_LoadOnlyResumeInfo(MYFILE *fp, GAME_INFO *game_info); -void Savegame_BSON_SaveToFile(MYFILE *fp, GAME_INFO *game_info); -bool Savegame_BSON_UpdateDeathCounters(MYFILE *fp, GAME_INFO *game_info); diff --git a/src/tr1/game/savegame/savegame_legacy.c b/src/tr1/game/savegame/savegame_legacy.c index 76a7bf522..34db1bd17 100644 --- a/src/tr1/game/savegame/savegame_legacy.c +++ b/src/tr1/game/savegame/savegame_legacy.c @@ -1,5 +1,3 @@ -#include "game/savegame/savegame_legacy.h" - #include "game/camera.h" #include "game/carrier.h" #include "game/effects.h" @@ -11,6 +9,7 @@ #include "game/level.h" #include "game/lot.h" #include "game/room.h" +#include "game/savegame.h" #include "game/shell.h" #include "game/stats.h" #include "global/const.h" @@ -83,7 +82,26 @@ static void M_ReadAmmoInfo(AMMO_INFO *ammo_info); static void M_ReadLara(LARA_INFO *lara); static void M_ReadLOT(LOT_INFO *lot); static void M_SetCurrentPosition(int32_t level_num); -static void M_ReadResumeInfo(MYFILE *fp, GAME_INFO *game_info); +static void M_ReadResumeInfo(MYFILE *fp); + +static const char *M_GetSaveFilePattern(void); +static bool M_FillInfo(MYFILE *fp, SAVEGAME_INFO *savegame_info); +static bool M_LoadFromFile(MYFILE *fp); +static bool M_LoadOnlyResumeInfo(MYFILE *fp); + +static SAVEGAME_STRATEGY m_Strategy = { + // clang-format off + .allow_load = true, + .allow_save = false, + .format = SAVEGAME_FORMAT_LEGACY, + .get_save_file_pattern_func = M_GetSaveFilePattern, + .fill_info_func = M_FillInfo, + .load_from_file_func = M_LoadFromFile, + .load_only_resume_info_func = M_LoadOnlyResumeInfo, + .save_to_file_func = nullptr, + .update_death_counters_func = nullptr, + // clang-format on +}; static bool M_ItemHasSaveFlags(const OBJECT *const obj, ITEM *const item) { @@ -275,10 +293,10 @@ static void M_ReadLara(LARA_INFO *const lara) M_ReadArm(&lara->left_arm); M_ReadArm(&lara->right_arm); - M_ReadAmmoInfo(&lara->pistols); - M_ReadAmmoInfo(&lara->magnums); - M_ReadAmmoInfo(&lara->uzis); - M_ReadAmmoInfo(&lara->shotgun); + M_ReadAmmoInfo(&lara->pistol_ammo); + M_ReadAmmoInfo(&lara->magnum_ammo); + M_ReadAmmoInfo(&lara->uzi_ammo); + M_ReadAmmoInfo(&lara->shotgun_ammo); M_ReadLOT(&lara->lot); } @@ -325,14 +343,13 @@ static void M_SetCurrentPosition(const int32_t level_num) for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; if (level->type == GFL_CURRENT) { - g_GameInfo.current[current_level->num] = g_GameInfo.current[i]; + Savegame_SetCurrentInfo(current_level->num, i); } } } -static void M_ReadResumeInfo(MYFILE *const fp, GAME_INFO *const game_info) +static void M_ReadResumeInfo(MYFILE *const fp) { - ASSERT(game_info->current != nullptr); const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < level_table->count; i++) { const GF_LEVEL *const level = &level_table->levels[i]; @@ -341,8 +358,8 @@ static void M_ReadResumeInfo(MYFILE *const fp, GAME_INFO *const game_info) current->magnum_ammo = M_ReadU16(); current->uzi_ammo = M_ReadU16(); current->shotgun_ammo = M_ReadU16(); - current->num_medis = M_ReadU8(); - current->num_big_medis = M_ReadU8(); + current->small_medipacks = M_ReadU8(); + current->large_medipacks = M_ReadU8(); current->num_scions = M_ReadU8(); current->gun_status = M_ReadS8(); current->equipped_gun_type = M_ReadS8(); @@ -351,10 +368,10 @@ static void M_ReadResumeInfo(MYFILE *const fp, GAME_INFO *const game_info) const uint16_t flags = M_ReadU16(); current->flags.available = flags & 1 ? 1 : 0; - current->flags.got_pistols = flags & 2 ? 1 : 0; - current->flags.got_magnums = flags & 4 ? 1 : 0; - current->flags.got_uzis = flags & 8 ? 1 : 0; - current->flags.got_shotgun = flags & 16 ? 1 : 0; + current->flags.has_pistols = flags & 2 ? 1 : 0; + current->flags.has_magnums = flags & 4 ? 1 : 0; + current->flags.has_uzis = flags & 8 ? 1 : 0; + current->flags.has_shotgun = flags & 16 ? 1 : 0; current->flags.costume = flags & 32 ? 1 : 0; // Gym and first level have special starting items. if (level == GF_GetFirstLevel() || level == GF_GetGymLevel()) { @@ -375,16 +392,19 @@ static void M_ReadResumeInfo(MYFILE *const fp, GAME_INFO *const game_info) resume_info->stats.secret_flags = temp_secret_flags; Stats_UpdateSecrets(&resume_info->stats); resume_info->stats.pickup_count = M_ReadU8(); - game_info->bonus_flag = M_ReadU8(); - game_info->death_count = -1; + const bool is_ng_plus = M_ReadU8() != 0; + if (is_ng_plus) { + Game_SetBonusFlag(GBF_NGPLUS); + } + resume_info->stats.death_count = -1; } -const char *Savegame_Legacy_GetSaveFilePattern(void) +static const char *M_GetSaveFilePattern(void) { return g_GameFlow.savegame_fmt_legacy; } -bool Savegame_Legacy_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info) +static bool M_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info) { File_Seek(fp, 0, SEEK_SET); @@ -422,10 +442,8 @@ bool Savegame_Legacy_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info) return true; } -bool Savegame_Legacy_LoadFromFile(MYFILE *const fp, GAME_INFO *const game_info) +static bool M_LoadFromFile(MYFILE *const fp) { - ASSERT(game_info != nullptr); - char *buffer = Memory_Alloc(File_Size(fp)); File_Seek(fp, 0, FILE_SEEK_SET); File_ReadData(fp, buffer, File_Size(fp)); @@ -439,7 +457,7 @@ bool Savegame_Legacy_LoadFromFile(MYFILE *const fp, GAME_INFO *const game_info) M_Skip(SAVEGAME_LEGACY_TITLE_SIZE); // level title M_Skip(sizeof(int32_t)); // save counter - M_ReadResumeInfo(fp, game_info); + M_ReadResumeInfo(fp); g_Lara.holsters_gun_type = LGT_UNKNOWN; g_Lara.back_gun_type = LGT_UNKNOWN; @@ -557,10 +575,8 @@ bool Savegame_Legacy_LoadFromFile(MYFILE *const fp, GAME_INFO *const game_info) return true; } -bool Savegame_Legacy_LoadOnlyResumeInfo(MYFILE *fp, GAME_INFO *game_info) +static bool M_LoadOnlyResumeInfo(MYFILE *const fp) { - ASSERT(game_info != nullptr); - char *buffer = Memory_Alloc(File_Size(fp)); File_Seek(fp, 0, FILE_SEEK_SET); File_ReadData(fp, buffer, File_Size(fp)); @@ -568,14 +584,10 @@ bool Savegame_Legacy_LoadOnlyResumeInfo(MYFILE *fp, GAME_INFO *game_info) M_Skip(SAVEGAME_LEGACY_TITLE_SIZE); // level title M_Skip(sizeof(int32_t)); // save counter - M_ReadResumeInfo(fp, game_info); + M_ReadResumeInfo(fp); Memory_FreePointer(&buffer); return true; } -bool Savegame_Legacy_UpdateDeathCounters( - MYFILE *const fp, GAME_INFO *const game_info) -{ - return false; -} +REGISTER_SAVEGAME_STRATEGY(m_Strategy) diff --git a/src/tr1/game/savegame/savegame_legacy.h b/src/tr1/game/savegame/savegame_legacy.h deleted file mode 100644 index ebcfcbf89..000000000 --- a/src/tr1/game/savegame/savegame_legacy.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "game/savegame.h" -#include "global/types.h" - -#include - -#include - -// TombATI implementation of savegames. - -const char *Savegame_Legacy_GetSaveFilePattern(void); -bool Savegame_Legacy_FillInfo(MYFILE *fp, SAVEGAME_INFO *info); -bool Savegame_Legacy_LoadFromFile(MYFILE *fp, GAME_INFO *game_info); -bool Savegame_Legacy_LoadOnlyResumeInfo(MYFILE *fp, GAME_INFO *game_info); -bool Savegame_Legacy_UpdateDeathCounters(MYFILE *fp, GAME_INFO *game_info); diff --git a/src/tr1/game/screen.c b/src/tr1/game/screen.c index 581ac77af..d0ee41efd 100644 --- a/src/tr1/game/screen.c +++ b/src/tr1/game/screen.c @@ -71,9 +71,10 @@ void Screen_Init(void) } // set the first resolution size to desktop size + const SHELL_SIZE display_size = Shell_GetCurrentDisplaySize(); res = &m_Resolutions[0]; - res->width = Shell_GetCurrentDisplayWidth(); - res->height = Shell_GetCurrentDisplayHeight(); + res->width = display_size.w; + res->height = display_size.h; // select matching resolution from config if (g_Config.rendering.resolution_width > 0 @@ -131,7 +132,7 @@ int32_t Screen_GetRenderScale(int32_t unit, RENDER_SCALE_REF ref) } else if (ref == RSR_BAR) { return M_GetRenderScaleBase(unit, 640, 480, g_Config.ui.bar_scale); } else { - return M_GetRenderScaleBase(unit, 640, 480, 0); + return M_GetRenderScaleBase(unit, 640, 480, 1.0f); } } diff --git a/src/tr1/game/screen.h b/src/tr1/game/screen.h index ffbe25b2f..280e9d493 100644 --- a/src/tr1/game/screen.h +++ b/src/tr1/game/screen.h @@ -3,8 +3,9 @@ #include typedef enum { - RSR_TEXT = 0, - RSR_BAR = 1, + RSR_TEXT, + RSR_BAR, + RSR_GENERIC, } RENDER_SCALE_REF; void Screen_Init(void); diff --git a/src/tr1/game/shell.c b/src/tr1/game/shell.c index 728f7456f..f892719ba 100644 --- a/src/tr1/game/shell.c +++ b/src/tr1/game/shell.c @@ -26,7 +26,7 @@ #include #include #include -#include +#include #include #include @@ -132,7 +132,6 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) Savegame_Shutdown(); Savegame_Init(); Savegame_ScanSavedGames(); - Savegame_FillAvailableSaves(&g_SavegameRequester); Savegame_HighlightNewestSlot(); } @@ -212,7 +211,6 @@ void Shell_Main(void) Savegame_Init(); Savegame_ScanSavedGames(); - Savegame_FillAvailableSaves(&g_SavegameRequester); Savegame_HighlightNewestSlot(); GameBuf_Init(); Console_Init(); diff --git a/src/tr1/game/spawn.c b/src/tr1/game/spawn.c index dfa547c23..8fe008f11 100644 --- a/src/tr1/game/spawn.c +++ b/src/tr1/game/spawn.c @@ -131,7 +131,7 @@ int16_t Spawn_RocketGun( effect->object_id = O_MISSILE_3; effect->frame_num = 0; effect->speed = ROCKET_SPEED; - effect->shade = HIGH_LIGHT; + effect->shade = SHADE_NEUTRAL; M_ShootAtLara(effect); } return effect_num; @@ -154,7 +154,7 @@ int16_t Spawn_GunShot( effect->counter = 3; effect->frame_num = 0; effect->object_id = O_GUN_FLASH; - effect->shade = HIGH_LIGHT; + effect->shade = SHADE_NEUTRAL; } return effect_num; } diff --git a/src/tr1/game/stats.h b/src/tr1/game/stats.h index 12d2f11d9..d36f32fa5 100644 --- a/src/tr1/game/stats.h +++ b/src/tr1/game/stats.h @@ -1,4 +1,5 @@ #pragma once #include "game/stats/common.h" -#include "game/stats/types.h" + +#include diff --git a/src/tr1/game/stats/common.c b/src/tr1/game/stats/common.c index 1c98b57b8..1af9562d1 100644 --- a/src/tr1/game/stats/common.c +++ b/src/tr1/game/stats/common.c @@ -18,7 +18,6 @@ #include #include -#define MAX_TEXTSTRINGS 10 #define USE_REAL_CLOCK 0 static int32_t m_CachedItemCount = 0; @@ -328,3 +327,15 @@ void Stats_AddDistanceTravelled(const XYZ_32 pos, const XYZ_32 last_pos) SQUARE(pos.z - last_pos.z) + SQUARE(pos.y - last_pos.y) + SQUARE(pos.x - last_pos.x)); } + +void Stats_AddDeath(void) +{ + const GF_LEVEL *const current_level = Game_GetCurrentLevel(); + RESUME_INFO *const current_info = Savegame_GetCurrentInfo(current_level); + current_info->stats.death_count++; + const int32_t save_slot = Savegame_GetBoundSlot(); + if (save_slot != -1) { + Savegame_UpdateDeathCounters( + save_slot, current_info->stats.death_count); + } +} diff --git a/src/tr1/game/stats/common.h b/src/tr1/game/stats/common.h index 5b1387323..13c63a954 100644 --- a/src/tr1/game/stats/common.h +++ b/src/tr1/game/stats/common.h @@ -1,8 +1,9 @@ #pragma once -#include "game/stats/types.h" #include "global/types.h" +#include + void Stats_ObserveRoomsLoad(void); void Stats_CalculateStats(void); int32_t Stats_GetPickups(void); @@ -11,7 +12,6 @@ int32_t Stats_GetSecrets(void); void Stats_ComputeFinal(GF_LEVEL_TYPE level_type, FINAL_STATS *stats); bool Stats_CheckAllSecretsCollected(GF_LEVEL_TYPE level_type); -void Stats_StartTimer(void); void Stats_UpdateTimer(void); void Stats_UpdateSecrets(LEVEL_STATS *stats); @@ -22,3 +22,4 @@ void Stats_AddAmmoHits(void); void Stats_AddAmmoUsed(void); void Stats_AddMedipacksUsed(double medipack_value); void Stats_AddDistanceTravelled(XYZ_32 pos, XYZ_32 last_pos); +void Stats_AddDeath(void); diff --git a/src/tr1/game/text.c b/src/tr1/game/text.c index 95f42ddf3..4227d1815 100644 --- a/src/tr1/game/text.c +++ b/src/tr1/game/text.c @@ -7,111 +7,9 @@ #include -#define TEXT_BOX_OFFSET 2 - -static RGBA_8888 m_MenuColorMap[MC_NUMBER_OF] = { - { 70, 30, 107, 230 }, // MC_PURPLE_C - { 70, 30, 107, 0 }, // MC_PURPLE_E - { 91, 46, 9, 255 }, // MC_BROWN_C - { 91, 46, 9, 0 }, // MC_BROWN_E - { 197, 197, 197, 255 }, // MC_GREY_C - { 45, 45, 45, 255 }, // MC_GREY_E - { 96, 96, 96, 255 }, // MC_GREY_TL - { 32, 32, 32, 255 }, // MC_GREY_TR - { 63, 63, 63, 255 }, // MC_GREY_BL - { 0, 0, 0, 255 }, // MC_GREY_BR - { 0, 0, 0, 255 }, // MC_BLACK - { 232, 192, 112, 255 }, // MC_GOLD_LIGHT - { 140, 112, 56, 255 }, // MC_GOLD_DARK -}; - -typedef struct { - int32_t x; - int32_t y; - int32_t w; - int32_t h; -} QUAD_INFO; - -static void M_DrawTextBackground( - UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t w, int32_t h, - TEXT_STYLE text_style); -static void M_DrawTextOutline( - UI_STYLE ui_style, int32_t sx, int32_t sy, int32_t w, int32_t h, - TEXT_STYLE text_style); - -static void M_DrawTextBackground( - const UI_STYLE ui_style, const int32_t sx, const int32_t sy, int32_t w, - int32_t h, const TEXT_STYLE text_style) -{ - if (ui_style == UI_STYLE_PC) { - Output_DrawScreenFBox(sx, sy, w, h); - return; - } - - // Make sure height and width divisible by 2. - w = 2 * ((w + 1) / 2); - h = 2 * ((h + 1) / 2); - Output_DrawScreenFBox(sx - 1, sy - 1, w + 1, h + 1); - - QUAD_INFO gradient_quads[4] = { { sx, sy, w / 2, h / 2 }, - { sx + w, sy, -w / 2, h / 2 }, - { sx, sy + h, w / 2, -h / 2 }, - { sx + w, sy + h, -w / 2, -h / 2 } }; - - if (text_style == TS_HEADING) { - for (int i = 0; i < 4; i++) { - Output_DrawScreenGradientQuad( - gradient_quads[i].x, gradient_quads[i].y, gradient_quads[i].w, - gradient_quads[i].h, Text_GetMenuColor(MC_BROWN_E), - Text_GetMenuColor(MC_BROWN_E), Text_GetMenuColor(MC_BROWN_E), - Text_GetMenuColor(MC_BROWN_C)); - } - } else if (text_style == TS_REQUESTED) { - for (int i = 0; i < 4; i++) { - Output_DrawScreenGradientQuad( - gradient_quads[i].x, gradient_quads[i].y, gradient_quads[i].w, - gradient_quads[i].h, Text_GetMenuColor(MC_PURPLE_E), - Text_GetMenuColor(MC_PURPLE_E), Text_GetMenuColor(MC_PURPLE_E), - Text_GetMenuColor(MC_PURPLE_C)); - } - } -} - -static void M_DrawTextOutline( - const UI_STYLE ui_style, const int32_t sx, const int32_t sy, int32_t w, - int32_t h, const TEXT_STYLE text_style) -{ - if (ui_style == UI_STYLE_PC) { - Output_DrawScreenFrame( - sx, sy, w, h, Text_GetMenuColor(MC_GOLD_DARK), - Text_GetMenuColor(MC_GOLD_LIGHT), TEXT_OUTLINE_THICKNESS); - return; - } - - if (text_style == TS_HEADING) { - Output_DrawGradientScreenBox( - sx, sy, w, h, Text_GetMenuColor(MC_BLACK), - Text_GetMenuColor(MC_BLACK), Text_GetMenuColor(MC_BLACK), - Text_GetMenuColor(MC_BLACK), TEXT_OUTLINE_THICKNESS); - } else if (text_style == TS_BACKGROUND) { - Output_DrawGradientScreenBox( - sx, sy, w, h, Text_GetMenuColor(MC_GREY_TL), - Text_GetMenuColor(MC_GREY_TR), Text_GetMenuColor(MC_GREY_BL), - Text_GetMenuColor(MC_GREY_BR), TEXT_OUTLINE_THICKNESS); - } else if (text_style == TS_REQUESTED) { - // Make sure height and width divisible by 2. - w = 2 * ((w + 1) / 2); - h = 2 * ((h + 1) / 2); - Output_DrawCentreGradientScreenBox( - sx, sy, w, h, Text_GetMenuColor(MC_GREY_E), - Text_GetMenuColor(MC_GREY_C), TEXT_OUTLINE_THICKNESS); - } -} - -RGBA_8888 Text_GetMenuColor(MENU_COLOR color) -{ - return m_MenuColorMap[color]; -} +#define TEXT_BOX_OFFSET_X 2 +#define TEXT_BOX_OFFSET_Y1 0 +#define TEXT_BOX_OFFSET_Y2 3 void Text_DrawText(TEXTSTRING *const text) { @@ -154,14 +52,14 @@ void Text_DrawText(TEXTSTRING *const text) y += Screen_GetResHeightDownscaled(RSR_TEXT); } - int32_t bxpos = text->background.offset.x + x - TEXT_BOX_OFFSET; + int32_t bxpos = text->background.offset.x + x - TEXT_BOX_OFFSET_X; int32_t bypos = - text->background.offset.y + y - TEXT_BOX_OFFSET * 2 - TEXT_HEIGHT; + text->background.offset.y + y + TEXT_BOX_OFFSET_Y1 - TEXT_HEIGHT_FIXED; int32_t sx; int32_t sy; - int32_t sh; - int32_t sv; + int32_t sh = Screen_GetRenderScale(text->scale.h, RSR_TEXT); + int32_t sv = Screen_GetRenderScale(text->scale.v, RSR_TEXT); const int32_t start_x = x; const GLYPH_INFO **glyph_ptr = text->glyphs; @@ -179,8 +77,6 @@ void Text_DrawText(TEXTSTRING *const text) sx = Screen_GetRenderScale(x, RSR_TEXT); sy = Screen_GetRenderScale(y, RSR_TEXT); - sh = Screen_GetRenderScale(text->scale.h, RSR_TEXT); - sv = Screen_GetRenderScale(text->scale.v, RSR_TEXT); if (glyph->role == GLYPH_COMPOUND) { const int32_t csx = sx @@ -216,14 +112,14 @@ void Text_DrawText(TEXTSTRING *const text) if (text->background.size.x) { bxpos += text_width / 2; bxpos -= text->background.size.x / 2; - bwidth = text->background.size.x + TEXT_BOX_OFFSET * 2; + bwidth = text->background.size.x + TEXT_BOX_OFFSET_X * 2; } else { - bwidth = text_width + TEXT_BOX_OFFSET * 2; + bwidth = text_width + TEXT_BOX_OFFSET_X * 2; } if (text->background.size.y) { bheight = text->background.size.y; } else { - bheight = TEXT_HEIGHT + 7; + bheight = TEXT_HEIGHT_FIXED + TEXT_BOX_OFFSET_Y2; } } @@ -232,9 +128,8 @@ void Text_DrawText(TEXTSTRING *const text) sy = Screen_GetRenderScale(bypos, RSR_TEXT); sh = Screen_GetRenderScale(bwidth, RSR_TEXT); sv = Screen_GetRenderScale(bheight, RSR_TEXT); - - M_DrawTextBackground( - g_Config.ui.menu_style, sx, sy, sh, sv, text->background.style); + Output_DrawTextBackground( + g_Config.ui.menu_style, sx, sy, sh, sv, 0, text->background.style); } if (text->flags.outline) { @@ -242,13 +137,12 @@ void Text_DrawText(TEXTSTRING *const text) sy = Screen_GetRenderScale(bypos, RSR_TEXT); sh = Screen_GetRenderScale(bwidth, RSR_TEXT); sv = Screen_GetRenderScale(bheight, RSR_TEXT); - - M_DrawTextOutline( - g_Config.ui.menu_style, sx, sy, sh, sv, text->outline.style); + Output_DrawTextOutline( + g_Config.ui.menu_style, sx, sy, sh, sv, 0, text->outline.style); } } int32_t Text_GetMaxLineLength(void) { - return Screen_GetResWidthDownscaled(RSR_TEXT) / (TEXT_HEIGHT * 0.75); + return Screen_GetResWidthDownscaled(RSR_TEXT) / (TEXT_HEIGHT_FIXED * 0.6); } diff --git a/src/tr1/game/text.h b/src/tr1/game/text.h index 418e844cd..b398cad9d 100644 --- a/src/tr1/game/text.h +++ b/src/tr1/game/text.h @@ -1,13 +1,5 @@ #pragma once -#include "global/types.h" - #include -#include - -#define TEXT_HEIGHT 11 // TODO: Get rid of this -#define TEXT_OUTLINE_THICKNESS 2 - -RGBA_8888 Text_GetMenuColor(MENU_COLOR color); void Text_DrawText(TEXTSTRING *text); diff --git a/src/tr1/game/ui/common.c b/src/tr1/game/ui/common.c index 0712f2cd5..b3cdf81f9 100644 --- a/src/tr1/game/ui/common.c +++ b/src/tr1/game/ui/common.c @@ -3,32 +3,22 @@ #include #include -#include - int32_t UI_GetCanvasWidth(void) { - return Screen_GetResWidthDownscaled(RSR_TEXT); + return Screen_GetResHeightDownscaled(RSR_GENERIC) * 16 / 9; } int32_t UI_GetCanvasHeight(void) { - return Screen_GetResHeightDownscaled(RSR_TEXT); + return Screen_GetResHeightDownscaled(RSR_GENERIC); } -UI_INPUT UI_TranslateInput(uint32_t system_keycode) +float UI_ScaleX(const float x) { - // clang-format off - switch (system_keycode) { - case SDLK_UP: return UI_KEY_UP; - case SDLK_DOWN: return UI_KEY_DOWN; - case SDLK_LEFT: return UI_KEY_LEFT; - case SDLK_RIGHT: return UI_KEY_RIGHT; - case SDLK_HOME: return UI_KEY_HOME; - case SDLK_END: return UI_KEY_END; - case SDLK_BACKSPACE: return UI_KEY_BACK; - case SDLK_RETURN: return UI_KEY_RETURN; - case SDLK_ESCAPE: return UI_KEY_ESCAPE; - } - // clang-format on - return -1; + return Screen_GetRenderScale(x * 0x10000, RSR_GENERIC) / (float)0x10000; +} + +float UI_ScaleY(const float y) +{ + return Screen_GetRenderScale(y * 0x10000, RSR_GENERIC) / (float)0x10000; } diff --git a/src/tr1/game/ui/dialogs/stats.c b/src/tr1/game/ui/dialogs/stats.c new file mode 100644 index 000000000..802514177 --- /dev/null +++ b/src/tr1/game/ui/dialogs/stats.c @@ -0,0 +1,268 @@ +#include "game/ui/dialogs/stats.h" + +#include "game/game_flow.h" +#include "game/game_string.h" +#include "game/savegame.h" +#include "game/stats.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +typedef enum { + M_ROW_KILLS, + M_ROW_PICKUPS, + M_ROW_SECRETS, + M_ROW_DEATHS, + M_ROW_TIMER, + M_ROW_AMMO, + M_ROW_MEDIPACKS_USED, + M_ROW_DISTANCE_TRAVELLED, +} M_ROW_ROLE; + +static void M_FormatTime(char *out, int32_t total_frames); +static void M_FormatDistance(char *const out, int32_t distance); +static void M_Row( + const UI_STATS_DIALOG_STATE *s, const char *key, const char *value); +static void M_RowFromRole( + const UI_STATS_DIALOG_STATE *s, M_ROW_ROLE role, const STATS_COMMON *stats); +static void M_CommonRows( + const UI_STATS_DIALOG_STATE *s, const STATS_COMMON *stats); +static void M_LevelStatsRows(const UI_STATS_DIALOG_STATE *s); +static void M_FinalStatsRows(const UI_STATS_DIALOG_STATE *s); +static const char *M_GetDialogTitle(const UI_STATS_DIALOG_STATE *s); +static void M_BeginDialog(const UI_STATS_DIALOG_STATE *s); +static void M_EndDialog(const UI_STATS_DIALOG_STATE *s); + +static void M_FormatTime(char *const out, const int32_t total_frames) +{ + const int32_t total_seconds = total_frames / LOGIC_FPS; + const int32_t hours = total_seconds / 3600; + const int32_t minutes = (total_seconds / 60) % 60; + const int32_t seconds = total_seconds % 60; + if (hours != 0) { + sprintf(out, "%d:%02d:%02d", hours, minutes, seconds); + } else { + sprintf(out, "%d:%02d", minutes, seconds); + } +} + +static void M_FormatDistance(char *const out, int32_t distance) +{ + distance /= 445; + if (distance < 1000) { + sprintf(out, "%dm", distance); + } else { + sprintf(out, "%d.%02dkm", distance / 1000, (distance % 1000) / 10); + } +} + +static void M_Row( + const UI_STATS_DIALOG_STATE *const s, const char *const key, + const char *const value) +{ + if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { + UI_BeginStack(UI_STACK_HORIZONTAL); + UI_Label(key); + UI_Label(" "); + UI_Label(value); + UI_EndStack(); + } else { + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .spacing = { .h = 30.0f }, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + }); + UI_Label(key); + UI_Label(value); + UI_EndStack(); + } +} + +static void M_RowFromRole( + const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role, + const STATS_COMMON *const stats) +{ + char buf[50]; + const char *const num_fmt = + g_Config.gameplay.stat_detail_mode == SDM_MINIMAL + ? GS(STATS_BASIC_FMT) + : GS(STATS_DETAIL_FMT); + + switch (role) { + case M_ROW_KILLS: + sprintf(buf, num_fmt, stats->kill_count, stats->max_kill_count); + M_Row(s, GS(STATS_KILLS), buf); + break; + + case M_ROW_PICKUPS: + sprintf(buf, num_fmt, stats->pickup_count, stats->max_pickup_count); + M_Row(s, GS(STATS_PICKUPS), buf); + break; + + case M_ROW_SECRETS: + sprintf( + buf, GS(STATS_DETAIL_FMT), stats->secret_count, + stats->max_secret_count); + M_Row(s, GS(STATS_SECRETS), buf); + break; + + case M_ROW_DEATHS: + sprintf(buf, GS(STATS_BASIC_FMT), stats->death_count); + M_Row(s, GS(STATS_DEATHS), buf); + break; + + case M_ROW_TIMER: + M_FormatTime(buf, stats->timer); + M_Row(s, GS(STATS_TIME_TAKEN), buf); + break; + + case M_ROW_AMMO: + sprintf(buf, GS(PAGINATION_NAV), stats->ammo_hits, stats->ammo_used); + M_Row(s, GS(STATS_AMMO), buf); + break; + + case M_ROW_MEDIPACKS_USED: + sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used); + M_Row(s, GS(STATS_MEDIPACKS_USED), buf); + break; + + case M_ROW_DISTANCE_TRAVELLED: + M_FormatDistance(buf, stats->distance_travelled); + M_Row(s, GS(STATS_DISTANCE_TRAVELLED), buf); + break; + + default: + break; + } +} + +static void M_CommonRows( + const UI_STATS_DIALOG_STATE *const s, const STATS_COMMON *const stats) +{ + if (g_Config.gameplay.stat_detail_mode == SDM_MINIMAL) { + M_RowFromRole(s, M_ROW_KILLS, stats); + M_RowFromRole(s, M_ROW_PICKUPS, stats); + M_RowFromRole(s, M_ROW_SECRETS, stats); + M_RowFromRole(s, M_ROW_TIMER, stats); + } else { + M_RowFromRole(s, M_ROW_TIMER, stats); + M_RowFromRole(s, M_ROW_SECRETS, stats); + M_RowFromRole(s, M_ROW_PICKUPS, stats); + M_RowFromRole(s, M_ROW_KILLS, stats); + if (g_Config.gameplay.stat_detail_mode == SDM_FULL) { + M_RowFromRole(s, M_ROW_AMMO, stats); + M_RowFromRole(s, M_ROW_MEDIPACKS_USED, stats); + M_RowFromRole(s, M_ROW_DISTANCE_TRAVELLED, stats); + } + } + + if (g_Config.gameplay.enable_deaths_counter && stats->death_count >= 0) { + // Always use sum of all levels for the deaths. + // Deaths get stored in the resume info for the level they happen + // on, so if the player dies in Vilcabamba and reloads Caves, they + // should still see an incremented death counter. + M_RowFromRole(s, M_ROW_DEATHS, stats); + } +} + +static void M_LevelStatsRows(const UI_STATS_DIALOG_STATE *const s) +{ + const GF_LEVEL *const current_level = + GF_GetLevel(GFLT_MAIN, s->args.level_num); + const RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(current_level); + const STATS_COMMON *const stats = (STATS_COMMON *)¤t_info->stats; + M_CommonRows(s, stats); +} + +static void M_FinalStatsRows(const UI_STATS_DIALOG_STATE *const s) +{ + FINAL_STATS final_stats; + const GF_LEVEL_TYPE level_type = + GF_GetLevel(GFLT_MAIN, s->args.level_num)->type; + Stats_ComputeFinal(level_type, &final_stats); + M_CommonRows(s, (STATS_COMMON *)&final_stats); +} + +static const char *M_GetDialogTitle(const UI_STATS_DIALOG_STATE *const s) +{ + switch (s->args.mode) { + case UI_STATS_DIALOG_MODE_LEVEL: + return GF_GetLevel(GFLT_MAIN, s->args.level_num)->title; + + case UI_STATS_DIALOG_MODE_FINAL: { + const GF_LEVEL_TYPE level_type = + GF_GetLevel(GFLT_MAIN, s->args.level_num)->type; + if (level_type == GFL_BONUS) { + return GS(STATS_BONUS_STATISTICS); + } + return GS(STATS_FINAL_STATISTICS); + } + } + + return nullptr; +} + +static void M_BeginDialog(const UI_STATS_DIALOG_STATE *const s) +{ + const char *const title = M_GetDialogTitle(s); + UI_BeginModal(0.5f, 0.5f); + if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .spacing = { .v = 11.0f }, + .align = { .h = UI_STACK_H_ALIGN_CENTER }, + }); + if (title != nullptr) { + UI_Label(title); + } + } else { + UI_BeginWindow(); + if (title != nullptr) { + UI_WindowTitle(title); + } + UI_BeginWindowBody(); + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .spacing = { .v = 4.0f }, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + }); + } +} + +static void M_EndDialog(const UI_STATS_DIALOG_STATE *const s) +{ + if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { + UI_EndStack(); + } else { + UI_EndStack(); + UI_EndWindowBody(); + UI_EndWindow(); + } + UI_EndModal(); +} + +void UI_StatsDialog(UI_STATS_DIALOG_STATE *const s) +{ + M_BeginDialog(s); + + switch (s->args.mode) { + case UI_STATS_DIALOG_MODE_LEVEL: + M_LevelStatsRows(s); + break; + case UI_STATS_DIALOG_MODE_FINAL: + M_FinalStatsRows(s); + break; + } + + M_EndDialog(s); +} diff --git a/src/tr1/game/ui/dialogs/stats.h b/src/tr1/game/ui/dialogs/stats.h new file mode 100644 index 000000000..00c84b406 --- /dev/null +++ b/src/tr1/game/ui/dialogs/stats.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/src/tr1/game/ui/widgets/paginator.c b/src/tr1/game/ui/widgets/paginator.c deleted file mode 100644 index 320d91da8..000000000 --- a/src/tr1/game/ui/widgets/paginator.c +++ /dev/null @@ -1,232 +0,0 @@ -#include "game/ui/widgets/paginator.h" - -#include "game/game_string.h" -#include "game/input.h" -#include "game/screen.h" -#include "game/sound.h" -#include "game/text.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#define TITLE_MARGIN 5 -#define WINDOW_MARGIN 10 -#define DIALOG_PADDING 5 -#define PADDING_SCALED (3.5 * (DIALOG_PADDING + WINDOW_MARGIN)) - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *window; - UI_WIDGET *outer_stack; - UI_WIDGET *bottom_stack; - UI_WIDGET *title; - UI_WIDGET *top_spacer; - UI_WIDGET *text; - UI_WIDGET *bottom_spacer; - UI_WIDGET *left_arrow; - UI_WIDGET *right_arrow; - UI_WIDGET *right_arrow_spacer; - UI_WIDGET *page_label; - int32_t current_page; - VECTOR *page_content; - bool shown; -} UI_PAGINATOR; - -static void M_DoLayout(UI_PAGINATOR *self); -static int32_t M_GetWidth(const UI_PAGINATOR *self); -static int32_t M_GetHeight(const UI_PAGINATOR *self); -static void M_SetPosition(UI_PAGINATOR *self, int32_t x, int32_t y); -static bool M_SelectPage(UI_PAGINATOR *const self, int32_t new_page); -static void M_Control(UI_PAGINATOR *self); -static void M_Draw(UI_PAGINATOR *self); -static void M_Free(UI_PAGINATOR *self); - -static void M_DoLayout(UI_PAGINATOR *const self) -{ - M_SetPosition( - self, (Screen_GetResWidthDownscaled(RSR_TEXT) - M_GetWidth(self)) / 2.0, - (Screen_GetResHeightDownscaled(RSR_TEXT) - M_GetHeight(self)) / 2.0); -} - -static int32_t M_GetWidth(const UI_PAGINATOR *const self) -{ - return self->window->get_width(self->window); -} - -static int32_t M_GetHeight(const UI_PAGINATOR *const self) -{ - return self->window->get_height(self->window); -} - -static void M_SetPosition(UI_PAGINATOR *const self, int32_t x, int32_t y) -{ - self->window->set_position(self->window, x, y); -} - -static bool M_SelectPage(UI_PAGINATOR *const self, const int32_t new_page) -{ - if (new_page == self->current_page || new_page < 0 - || new_page >= self->page_content->count) { - return false; - } - - self->current_page = new_page; - UI_Label_ChangeText( - self->text, - *(char **)Vector_Get(self->page_content, self->current_page)); - - char page_indicator[100]; - sprintf( - page_indicator, GS(PAGINATION_NAV), self->current_page + 1, - self->page_content->count); - UI_Label_ChangeText(self->page_label, page_indicator); - UI_Label_SetVisible(self->left_arrow, self->current_page > 0); - UI_Label_SetVisible( - self->right_arrow, self->current_page < self->page_content->count - 1); - - return true; -} - -static void M_Control(UI_PAGINATOR *const self) -{ - const int32_t page_shift = - g_InputDB.menu_left ? -1 : (g_InputDB.menu_right ? 1 : 0); - if (M_SelectPage(self, self->current_page + page_shift)) { - Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS); - } - - if (self->window->control != nullptr) { - self->window->control(self->window); - } -} - -static void M_Draw(UI_PAGINATOR *const self) -{ - if (self->shown && self->window->draw != nullptr) { - self->window->draw(self->window); - } -} - -static void M_Free(UI_PAGINATOR *const self) -{ - for (int32_t i = self->page_content->count - 1; i >= 0; i--) { - char *const page = *(char **)Vector_Get(self->page_content, i); - Memory_Free(page); - } - Vector_Free(self->page_content); - - self->text->free(self->text); - self->top_spacer->free(self->top_spacer); - self->title->free(self->title); - if (self->bottom_stack != nullptr) { - self->bottom_spacer->free(self->bottom_spacer); - self->left_arrow->free(self->left_arrow); - self->right_arrow->free(self->right_arrow); - self->right_arrow_spacer->free(self->right_arrow_spacer); - self->page_label->free(self->page_label); - self->bottom_stack->free(self->bottom_stack); - } - self->outer_stack->free(self->outer_stack); - self->window->free(self->window); - Memory_Free(self); -} - -UI_WIDGET *UI_Paginator_Create( - const char *const title, const char *const text, const int32_t max_lines) -{ - UI_PAGINATOR *const self = Memory_Alloc(sizeof(UI_PAGINATOR)); - self->vtable = (UI_WIDGET_VTABLE) { - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->shown = !String_IsEmpty(text); - - self->outer_stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - - const char *upper_title = String_ToUpper(title); - self->title = - UI_Label_Create(upper_title, UI_LABEL_AUTO_SIZE, TEXT_HEIGHT_FIXED); - Memory_FreePointer(&upper_title); - UI_Stack_AddChild(self->outer_stack, self->title); - - self->top_spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN); - UI_Stack_AddChild(self->outer_stack, self->top_spacer); - - const char *wrapped = - String_WordWrap(text, Text_GetMaxLineLength() - PADDING_SCALED); - self->page_content = String_Paginate(wrapped, max_lines); - self->current_page = 0; - Memory_FreePointer(&wrapped); - - self->text = UI_Label_Create( - *(char **)Vector_Get(self->page_content, 0), UI_LABEL_AUTO_SIZE, - UI_LABEL_AUTO_SIZE); - UI_Stack_AddChild(self->outer_stack, self->text); - - if (self->page_content->count > 1) { - self->bottom_spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN * 3); - UI_Stack_AddChild(self->outer_stack, self->bottom_spacer); - - self->bottom_stack = UI_Stack_Create( - UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - UI_Stack_AddChild(self->outer_stack, self->bottom_stack); - - self->left_arrow = - UI_Label_Create("\\{button left}", 22, TEXT_HEIGHT_FIXED); - self->right_arrow = - UI_Label_Create("\\{button right}", 16, TEXT_HEIGHT_FIXED); - self->right_arrow_spacer = UI_Spacer_Create(6, TEXT_HEIGHT_FIXED); - self->page_label = - UI_Label_Create("", UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE); - UI_Stack_AddChild(self->bottom_stack, self->left_arrow); - UI_Stack_AddChild(self->bottom_stack, self->page_label); - UI_Stack_AddChild(self->bottom_stack, self->right_arrow_spacer); - UI_Stack_AddChild(self->bottom_stack, self->right_arrow); - - UI_Stack_SetHAlign(self->bottom_stack, UI_STACK_H_ALIGN_RIGHT); - } - - self->window = UI_Window_Create( - self->outer_stack, DIALOG_PADDING, DIALOG_PADDING, DIALOG_PADDING * 2, - DIALOG_PADDING); - - // Ensure minimum width for page navigation as text content may be empty. - int32_t max_width = - MAX(self->page_content->count == 1 ? 0 : 100, - UI_Label_MeasureTextWidth(self->title)); - int32_t max_nav_width = 0; - int32_t max_height = 0; - for (int32_t i = 0; i < self->page_content->count; i++) { - M_SelectPage(self, i); - max_width = MAX(max_width, UI_Label_MeasureTextWidth(self->text)); - max_height = MAX(max_height, UI_Label_MeasureTextHeight(self->text)); - if (self->bottom_stack != nullptr) { - max_nav_width = - MAX(max_nav_width, UI_Label_MeasureTextWidth(self->page_label)); - } - } - - UI_Label_SetSize(self->text, max_width, max_height); - if (self->bottom_stack != nullptr) { - UI_Stack_SetSize(self->bottom_stack, max_width, UI_STACK_AUTO_SIZE); - UI_Label_SetSize(self->page_label, max_nav_width, UI_LABEL_AUTO_SIZE); - } - - M_SelectPage(self, 0); - M_DoLayout(self); - - return (UI_WIDGET *)self; -} diff --git a/src/tr1/game/ui/widgets/paginator.h b/src/tr1/game/ui/widgets/paginator.h deleted file mode 100644 index 3dbbf2d2b..000000000 --- a/src/tr1/game/ui/widgets/paginator.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include - -UI_WIDGET *UI_Paginator_Create( - const char *title, const char *text, int32_t max_lines); diff --git a/src/tr1/game/ui/widgets/stats_dialog.c b/src/tr1/game/ui/widgets/stats_dialog.c deleted file mode 100644 index f671baf03..000000000 --- a/src/tr1/game/ui/widgets/stats_dialog.c +++ /dev/null @@ -1,385 +0,0 @@ -#include "game/ui/widgets/stats_dialog.h" - -#include "game/game_flow.h" -#include "game/game_string.h" -#include "game/input.h" -#include "game/stats.h" -#include "global/vars.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#define ROW_HEIGHT_BARE 25 -#define ROW_HEIGHT_BORDERED 18 -#define ROW_WIDTH_BORDERED 200 -#define ROW_WIDTH_BORDERED_FULL 315 - -typedef enum { - M_ROW_KILLS, - M_ROW_PICKUPS, - M_ROW_SECRETS, - M_ROW_DEATHS, - M_ROW_TIMER, - M_ROW_AMMO, - M_ROW_MEDIPACKS_USED, - M_ROW_DISTANCE_TRAVELLED, -} M_ROW_ROLE; - -typedef struct { - M_ROW_ROLE role; - UI_WIDGET *stack; - UI_WIDGET *key_label; - UI_WIDGET *value_label; -} M_ROW; - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_STATS_DIALOG_ARGS args; - GF_LEVEL_TYPE level_type; - int32_t listener; - - int32_t row_count; - UI_WIDGET *title; - UI_WIDGET *stack; - UI_WIDGET *window; - UI_WIDGET *root; // just a pointer to either stack or window - M_ROW *rows; -} UI_STATS_DIALOG; - -static void M_FormatTime(char *out, int32_t total_frames); -static const char *M_GetDialogTitle(UI_STATS_DIALOG *self); -static void M_AddRow( - UI_STATS_DIALOG *self, M_ROW_ROLE role, const char *key, const char *value); -static void M_AddRowFromRole( - UI_STATS_DIALOG *self, M_ROW_ROLE role, const STATS_COMMON *stats, - const GAME_INFO *game_info); -static void M_AddCommonRows( - UI_STATS_DIALOG *self, const STATS_COMMON *stats, - const GAME_INFO *game_info); -static void M_AddLevelStatsRows(UI_STATS_DIALOG *self); -static void M_AddFinalStatsRows(UI_STATS_DIALOG *self); -static void M_UpdateTimerRow(UI_STATS_DIALOG *self); -static void M_DoLayout(UI_STATS_DIALOG *self); -static void M_HandleLayoutUpdate(const EVENT *event, void *data); - -static int32_t M_GetWidth(const UI_STATS_DIALOG *self); -static int32_t M_GetHeight(const UI_STATS_DIALOG *self); -static void M_SetPosition(UI_STATS_DIALOG *self, int32_t x, int32_t y); -static void M_Control(UI_STATS_DIALOG *self); -static void M_Draw(UI_STATS_DIALOG *self); -static void M_Free(UI_STATS_DIALOG *self); - -static void M_FormatTime(char *const out, const int32_t total_frames) -{ - const int32_t total_seconds = total_frames / LOGIC_FPS; - const int32_t hours = total_seconds / 3600; - const int32_t minutes = (total_seconds / 60) % 60; - const int32_t seconds = total_seconds % 60; - if (hours != 0) { - sprintf(out, "%d:%02d:%02d", hours, minutes, seconds); - } else { - sprintf(out, "%d:%02d", minutes, seconds); - } -} - -static const char *M_GetDialogTitle(UI_STATS_DIALOG *const self) -{ - switch (self->args.mode) { - case UI_STATS_DIALOG_MODE_LEVEL: - return GF_GetLevel(GFLT_MAIN, self->args.level_num)->title; - - case UI_STATS_DIALOG_MODE_FINAL: - return self->level_type == GFL_BONUS ? GS(STATS_BONUS_STATISTICS) - : GS(STATS_FINAL_STATISTICS); - } - - return nullptr; -} - -static void M_AddRow( - UI_STATS_DIALOG *const self, const M_ROW_ROLE role, const char *const key, - const char *const value) -{ - self->row_count++; - self->rows = Memory_Realloc(self->rows, sizeof(M_ROW) * self->row_count); - M_ROW *const row = &self->rows[self->row_count - 1]; - row->role = role; - - // create a stack - int32_t row_height; - if (self->args.style == UI_STATS_DIALOG_STYLE_BARE) { - row_height = ROW_HEIGHT_BARE; - row->stack = UI_Stack_Create( - UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, row_height); - } else { - row_height = ROW_HEIGHT_BORDERED; - const int32_t row_width = g_Config.gameplay.stat_detail_mode == SDM_FULL - ? ROW_WIDTH_BORDERED_FULL - : ROW_WIDTH_BORDERED; - row->stack = - UI_Stack_Create(UI_STACK_LAYOUT_HORIZONTAL, row_width, row_height); - UI_Stack_SetHAlign(row->stack, UI_STACK_H_ALIGN_DISTRIBUTE); - } - - // create a key label; append space for the bare style - char key2[strlen(key) + 2]; - sprintf( - key2, self->args.style == UI_STATS_DIALOG_STYLE_BARE ? "%s " : "%s", - key); - row->key_label = UI_Label_Create(key2, UI_LABEL_AUTO_SIZE, row_height); - - // create a value label - row->value_label = UI_Label_Create(value, UI_LABEL_AUTO_SIZE, row_height); - - UI_Stack_AddChild(row->stack, row->key_label); - UI_Stack_AddChild(row->stack, row->value_label); - UI_Stack_AddChild(self->stack, row->stack); -} - -static void M_AddRowFromRole( - UI_STATS_DIALOG *const self, const M_ROW_ROLE role, - const STATS_COMMON *const stats, const GAME_INFO *const game_info) -{ - char buf[50]; - const char *const num_fmt = - g_Config.gameplay.stat_detail_mode == SDM_MINIMAL - ? GS(STATS_BASIC_FMT) - : GS(STATS_DETAIL_FMT); - - switch (role) { - case M_ROW_KILLS: - sprintf(buf, num_fmt, stats->kill_count, stats->max_kill_count); - M_AddRow(self, role, GS(STATS_KILLS), buf); - break; - - case M_ROW_PICKUPS: - sprintf(buf, num_fmt, stats->pickup_count, stats->max_pickup_count); - M_AddRow(self, role, GS(STATS_PICKUPS), buf); - break; - - case M_ROW_SECRETS: - sprintf( - buf, GS(STATS_DETAIL_FMT), stats->secret_count, - stats->max_secret_count); - M_AddRow(self, role, GS(STATS_SECRETS), buf); - break; - - case M_ROW_DEATHS: - sprintf(buf, GS(STATS_BASIC_FMT), game_info->death_count); - M_AddRow(self, role, GS(STATS_DEATHS), buf); - break; - - case M_ROW_TIMER: - M_FormatTime(buf, stats->timer); - M_AddRow(self, role, GS(STATS_TIME_TAKEN), buf); - break; - - case M_ROW_AMMO: - sprintf(buf, GS(PAGINATION_NAV), stats->ammo_hits, stats->ammo_used); - M_AddRow(self, role, GS(STATS_AMMO), buf); - break; - - case M_ROW_MEDIPACKS_USED: - sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used); - M_AddRow(self, role, GS(STATS_MEDIPACKS_USED), buf); - break; - - case M_ROW_DISTANCE_TRAVELLED: - const int32_t distance_travelled = stats->distance_travelled / 445; - if (distance_travelled < 1000) { - sprintf(buf, "%dm", distance_travelled); - } else { - sprintf( - buf, "%d.%02dkm", distance_travelled / 1000, - (distance_travelled % 1000) / 10); - } - M_AddRow(self, role, GS(STATS_DISTANCE_TRAVELLED), buf); - break; - - default: - break; - } -} - -static void M_AddCommonRows( - UI_STATS_DIALOG *const self, const STATS_COMMON *const stats, - const GAME_INFO *const game_info) -{ - if (g_Config.gameplay.stat_detail_mode == SDM_MINIMAL) { - M_AddRowFromRole(self, M_ROW_KILLS, stats, game_info); - M_AddRowFromRole(self, M_ROW_PICKUPS, stats, game_info); - M_AddRowFromRole(self, M_ROW_SECRETS, stats, game_info); - M_AddRowFromRole(self, M_ROW_TIMER, stats, game_info); - } else { - M_AddRowFromRole(self, M_ROW_TIMER, stats, game_info); - M_AddRowFromRole(self, M_ROW_SECRETS, stats, game_info); - M_AddRowFromRole(self, M_ROW_PICKUPS, stats, game_info); - M_AddRowFromRole(self, M_ROW_KILLS, stats, game_info); - if (g_Config.gameplay.stat_detail_mode == SDM_FULL) { - M_AddRowFromRole(self, M_ROW_AMMO, stats, game_info); - M_AddRowFromRole(self, M_ROW_MEDIPACKS_USED, stats, game_info); - M_AddRowFromRole(self, M_ROW_DISTANCE_TRAVELLED, stats, game_info); - } - } - - if (g_Config.gameplay.enable_deaths_counter - && game_info->death_count >= 0) { - // Always use sum of all levels for the deaths. - // Deaths get stored in the resume info for the level they happen - // on, so if the player dies in Vilcabamba and reloads Caves, they - // should still see an incremented death counter. - M_AddRowFromRole(self, M_ROW_DEATHS, stats, game_info); - } -} - -static void M_AddLevelStatsRows(UI_STATS_DIALOG *const self) -{ - const STATS_COMMON *stats = - (STATS_COMMON *)&g_GameInfo.current[self->args.level_num].stats; - M_AddCommonRows(self, stats, &g_GameInfo); -} - -static void M_AddFinalStatsRows(UI_STATS_DIALOG *const self) -{ - FINAL_STATS final_stats; - Stats_ComputeFinal(self->level_type, &final_stats); - M_AddCommonRows(self, (STATS_COMMON *)&final_stats, &g_GameInfo); -} - -static void M_UpdateTimerRow(UI_STATS_DIALOG *const self) -{ - if (self->args.mode != UI_STATS_DIALOG_MODE_LEVEL) { - return; - } - - for (int32_t i = 0; i < self->row_count; i++) { - if (self->rows[i].role != M_ROW_TIMER) { - continue; - } - char buf[50]; - M_FormatTime(buf, g_GameInfo.current[self->args.level_num].stats.timer); - UI_Label_ChangeText(self->rows[i].value_label, buf); - return; - } -} - -static void M_DoLayout(UI_STATS_DIALOG *const self) -{ - M_SetPosition( - self, (UI_GetCanvasWidth() - M_GetWidth(self)) / 2, - (UI_GetCanvasHeight() - M_GetHeight(self)) / 2); -} - -static void M_HandleLayoutUpdate(const EVENT *event, void *data) -{ - UI_STATS_DIALOG *const self = (UI_STATS_DIALOG *)data; - M_DoLayout(self); -} - -static int32_t M_GetWidth(const UI_STATS_DIALOG *const self) -{ - return self->root->get_width(self->root); -} - -static int32_t M_GetHeight(const UI_STATS_DIALOG *const self) -{ - return self->root->get_height(self->root); -} - -static void M_SetPosition( - UI_STATS_DIALOG *const self, const int32_t x, const int32_t y) -{ - self->root->set_position(self->root, x, y); -} - -static void M_Control(UI_STATS_DIALOG *const self) -{ - if (self->root->control != nullptr) { - self->root->control(self->root); - } - M_UpdateTimerRow(self); -} - -static void M_Draw(UI_STATS_DIALOG *const self) -{ - if (self->root->draw != nullptr) { - self->root->draw(self->root); - } -} - -static void M_Free(UI_STATS_DIALOG *const self) -{ - if (self->title != nullptr) { - self->title->free(self->title); - } - for (int32_t i = 0; i < self->row_count; i++) { - self->rows[i].key_label->free(self->rows[i].key_label); - self->rows[i].value_label->free(self->rows[i].value_label); - self->rows[i].stack->free(self->rows[i].stack); - } - if (self->window != nullptr) { - self->window->free(self->window); - } - self->stack->free(self->stack); - UI_Events_Unsubscribe(self->listener); - Memory_Free(self); -} - -UI_WIDGET *UI_StatsDialog_Create(const UI_STATS_DIALOG_ARGS args) -{ - UI_STATS_DIALOG *const self = Memory_Alloc(sizeof(UI_STATS_DIALOG)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->args = args; - self->level_type = GF_GetLevel(GFLT_MAIN, self->args.level_num)->type; - - self->row_count = 0; - self->rows = nullptr; - self->stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - UI_Stack_SetHAlign(self->stack, UI_STACK_H_ALIGN_CENTER); - - self->listener = UI_Events_Subscribe( - "layout_update", nullptr, M_HandleLayoutUpdate, self); - - const char *title = M_GetDialogTitle(self); - switch (self->args.style) { - case UI_STATS_DIALOG_STYLE_BARE: - if (title != nullptr) { - self->title = - UI_Label_Create(title, UI_LABEL_AUTO_SIZE, ROW_HEIGHT_BARE); - UI_Stack_AddChild(self->stack, self->title); - } - self->root = self->stack; - break; - - case UI_STATS_DIALOG_STYLE_BORDERED: - self->window = UI_Window_Create(self->stack, 8, 8, 8, 8); - UI_Window_SetTitle(self->window, title); - self->root = self->window; - break; - } - - if (self->args.mode == UI_STATS_DIALOG_MODE_LEVEL) { - M_AddLevelStatsRows(self); - } else if (self->args.mode == UI_STATS_DIALOG_MODE_FINAL) { - M_AddFinalStatsRows(self); - } - - M_DoLayout(self); - return (UI_WIDGET *)self; -} diff --git a/src/tr1/game/ui/widgets/stats_dialog.h b/src/tr1/game/ui/widgets/stats_dialog.h deleted file mode 100644 index 96afd51a2..000000000 --- a/src/tr1/game/ui/widgets/stats_dialog.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include diff --git a/src/tr1/game/viewport.c b/src/tr1/game/viewport.c index b71c18503..98e3e2cb4 100644 --- a/src/tr1/game/viewport.c +++ b/src/tr1/game/viewport.c @@ -1,5 +1,6 @@ #include "game/viewport.h" +#include "game/output.h" #include "game/screen.h" #include "global/const.h" #include "global/vars.h" @@ -21,8 +22,8 @@ void Viewport_Init(int32_t x, int32_t y, int32_t width, int32_t height) { m_MinX = x; m_MinY = y; - m_MaxX = x + width - 1; - m_MaxY = y + height - 1; + m_MaxX = x + width; + m_MaxY = y + height; m_CenterX = (m_MinX + m_MaxX) / 2; m_CenterY = (m_MinY + m_MaxY) / 2; m_Width = width; @@ -87,4 +88,5 @@ int16_t Viewport_GetUserFOV(void) void Viewport_SetFOV(int16_t fov) { m_CurrentFOV = fov; + Output_ObserveFOVChange(); } diff --git a/src/tr1/global/const.h b/src/tr1/global/const.h index 2f89a7269..8f8a0c3b5 100644 --- a/src/tr1/global/const.h +++ b/src/tr1/global/const.h @@ -6,7 +6,7 @@ #define PHD_ONE 0x10000 #define MAX_REQLINES 18 -#define NUM_SLOTS 32 +#define LOT_SLOT_COUNT 32 #define MAX_SECRETS 16 #define LARA_MAX_HITPOINTS 1000 #define LARA_MAX_AIR 1800 @@ -41,17 +41,6 @@ #define DAMAGE_START 140 #define DAMAGE_LENGTH 14 #define NO_CAMERA (-1) -#define PELLET_SCATTER (20 * DEG_1) -#define NUM_SG_SHELLS 2 -#define GUN_AMMO_CLIP 16 -#define MAGNUM_AMMO_CLIP 25 -#define UZI_AMMO_CLIP 50 -#define SHOTGUN_AMMO_CLIP 6 -#define GUN_AMMO_QTY (GUN_AMMO_CLIP * 2) -#define MAGNUM_AMMO_QTY (MAGNUM_AMMO_CLIP * 2) -#define UZI_AMMO_QTY (UZI_AMMO_CLIP * 2) -#define SHOTGUN_AMMO_QTY (SHOTGUN_AMMO_CLIP * NUM_SG_SHELLS) -#define NUM_EFFECTS 1000 #define DEATH_WAIT (10 * LOGIC_FPS) #define DEATH_WAIT_MIN (2 * LOGIC_FPS) #define MAX_HEAD_ROTATION (50 * DEG_1) // = 9100 @@ -66,8 +55,6 @@ #define MIN_HEAD_TILT_SURF (-40 * DEG_1) // = -7280 #define DIVE_WAIT 10 #define STEPUP_HEIGHT ((STEP_L * 3) / 2) // = 384 -#define MAX_HEAD_CHANGE (DEG_1 * 5) // = 910 -#define MAX_TILT (DEG_1 * 3) // = 546 #define CAM_A_HANG 0 #define CAM_E_HANG (-60 * DEG_1) // = -10920 #define CAM_WADE_ELEVATION (-22 * DEG_1) // = -4004 @@ -82,13 +69,8 @@ #define NO_BAD_NEG NO_HEIGHT #define BAD_JUMP_CEILING ((STEP_L * 3) / 4) // = 192 #define MAX_WIBBLE 2 -#define MAX_SHADE 0x300 -#define MAX_LIGHTING 0x1FFF #define NO_VERT_MOVE 0x2000 #define NO_BOX (-1) -#define ATTACK_RANGE SQUARE(WALL_L * 3) // = 9437184 -#define ESCAPE_CHANCE 2048 -#define RECOVER_CHANCE 256 #define PASSPORT_FOV 65 #define PICKUPS_FOV 65 @@ -105,8 +87,6 @@ #define OPTION_RING_OBJECTS 4 #define TITLE_RING_OBJECTS 5 #define CAMERA_2_RING 598 -#define LOW_LIGHT 0x1400 // = 5120 -#define HIGH_LIGHT 0x1000 // = 4096 #if _MSC_VER > 0x500 #define strdup _strdup // fixes error about POSIX function diff --git a/src/tr1/global/enum_map.def b/src/tr1/global/enum_map.def index 1367705f0..4dc74c1d0 100644 --- a/src/tr1/global/enum_map.def +++ b/src/tr1/global/enum_map.def @@ -30,10 +30,6 @@ ENUM_MAP_DEFINE(TARGET_LOCK_MODE, TLM_FULL, "full-lock") ENUM_MAP_DEFINE(TARGET_LOCK_MODE, TLM_SEMI, "semi-lock") ENUM_MAP_DEFINE(TARGET_LOCK_MODE, TLM_NONE, "no-lock") -ENUM_MAP_DEFINE(MUSIC_LOAD_CONDITION, MUSIC_LOAD_NEVER, "never") -ENUM_MAP_DEFINE(MUSIC_LOAD_CONDITION, MUSIC_LOAD_NON_AMBIENT, "non-ambient") -ENUM_MAP_DEFINE(MUSIC_LOAD_CONDITION, MUSIC_LOAD_ALWAYS, "always") - ENUM_MAP_DEFINE(STAT_DETAIL_MODE, SDM_MINIMAL, "minimal") ENUM_MAP_DEFINE(STAT_DETAIL_MODE, SDM_DETAILED, "detailed") ENUM_MAP_DEFINE(STAT_DETAIL_MODE, SDM_FULL, "full") diff --git a/src/tr1/global/types.h b/src/tr1/global/types.h index acc9c3246..54dd99625 100644 --- a/src/tr1/global/types.h +++ b/src/tr1/global/types.h @@ -1,6 +1,5 @@ #pragma once -#include "game/stats/types.h" #include "global/const.h" #include @@ -90,11 +89,6 @@ typedef enum { BT_PROGRESS = 3, } BAR_TYPE; -typedef enum { - GBF_NGPLUS = 1 << 0, - GBF_JAPANESE = 1 << 1, -} GAME_BONUS_FLAG; - typedef enum { PASSPORT_MODE_BROWSE = 0, PASSPORT_MODE_LOAD_GAME = 1, @@ -136,39 +130,6 @@ typedef struct { } DOORPOS_DATA; typedef struct { - int32_t lara_hitpoints; - uint16_t pistol_ammo; - uint16_t magnum_ammo; - uint16_t uzi_ammo; - uint16_t shotgun_ammo; - uint8_t num_medis; - uint8_t num_big_medis; - uint8_t num_scions; - int8_t gun_status; - LARA_GUN_TYPE equipped_gun_type; - LARA_GUN_TYPE holsters_gun_type; - LARA_GUN_TYPE back_gun_type; - union { - uint16_t all; - struct { - uint16_t available : 1; - uint16_t got_pistols : 1; - uint16_t got_magnums : 1; - uint16_t got_uzis : 1; - uint16_t got_shotgun : 1; - uint16_t costume : 1; - }; - } flags; - LEVEL_STATS stats; -} RESUME_INFO; - -typedef struct { - RESUME_INFO *current; - int32_t death_count; - - uint8_t bonus_flag; - bool bonus_level_unlock; - int16_t save_initial_version; PASSPORT_MODE passport_selection; int32_t select_save_slot; int32_t select_level_num; @@ -186,23 +147,6 @@ typedef struct { bool ask_for_save; } GAME_INFO; -typedef enum { - MC_PURPLE_C, - MC_PURPLE_E, - MC_BROWN_C, - MC_BROWN_E, - MC_GREY_C, - MC_GREY_E, - MC_GREY_TL, - MC_GREY_TR, - MC_GREY_BL, - MC_GREY_BR, - MC_BLACK, - MC_GOLD_LIGHT, - MC_GOLD_DARK, - MC_NUMBER_OF, -} MENU_COLOR; - typedef struct { int32_t xv; int32_t yv; @@ -222,29 +166,3 @@ typedef struct { int16_t flash_time; int16_t sample_num; } WEAPON_INFO; - -typedef struct { - bool is_blocked; - char *content_text; - TEXTSTRING *content; -} REQUESTER_ITEM; - -typedef struct { - uint16_t items_used; - uint16_t max_items; - uint16_t requested; - uint16_t vis_lines; - int16_t line_offset; - int16_t line_old_offset; - uint16_t pix_width; - uint16_t line_height; - bool is_blockable; - int16_t x; - int16_t y; - char *heading_text; - TEXTSTRING *heading; - TEXTSTRING *background; - TEXTSTRING *moreup; - TEXTSTRING *moredown; - REQUESTER_ITEM *items; -} REQUEST_INFO; diff --git a/src/tr1/global/vars.c b/src/tr1/global/vars.c index 5ecb4065c..6adf7b098 100644 --- a/src/tr1/global/vars.c +++ b/src/tr1/global/vars.c @@ -15,13 +15,9 @@ float g_FltResZBuf; LARA_INFO g_Lara = {}; ITEM *g_LaraItem = nullptr; GAME_INFO g_GameInfo = { .select_save_slot = -1 }; -int32_t g_SavedGamesCount = 0; -int32_t g_SaveCounter = 0; bool g_LevelComplete = false; int32_t g_OverlayFlag = 0; -INVENTORY_MODE g_InvMode; - #ifndef MESON_BUILD const char *g_TRXVersion = "TR1X (non-Docker build)"; #endif diff --git a/src/tr1/global/vars.h b/src/tr1/global/vars.h index b0d83e4e5..d7e0a1b4c 100644 --- a/src/tr1/global/vars.h +++ b/src/tr1/global/vars.h @@ -23,11 +23,5 @@ extern int32_t g_FPSCounter; extern LARA_INFO g_Lara; extern ITEM *g_LaraItem; extern GAME_INFO g_GameInfo; -extern int32_t g_SavedGamesCount; -extern int32_t g_SaveCounter; extern bool g_LevelComplete; extern int32_t g_OverlayFlag; - -extern REQUEST_INFO g_SavegameRequester; - -extern INVENTORY_MODE g_InvMode; diff --git a/src/tr1/meson.build b/src/tr1/meson.build index f7a688658..277258aec 100644 --- a/src/tr1/meson.build +++ b/src/tr1/meson.build @@ -194,15 +194,10 @@ sources = [ 'game/objects/effects/splash.c', 'game/objects/effects/twinkle.c', 'game/objects/general/boat.c', - 'game/objects/general/bridge_common.c', - 'game/objects/general/bridge_flat.c', - 'game/objects/general/bridge_tilt1.c', - 'game/objects/general/bridge_tilt2.c', 'game/objects/general/cabin.c', 'game/objects/general/camera_target.c', 'game/objects/general/cog.c', 'game/objects/general/door.c', - 'game/objects/general/drawbridge.c', 'game/objects/general/earthquake.c', 'game/objects/general/keyhole.c', 'game/objects/general/moving_bar.c', @@ -214,7 +209,6 @@ sources = [ 'game/objects/general/scion4.c', 'game/objects/general/scion_holder.c', 'game/objects/general/switch.c', - 'game/objects/general/trapdoor.c', 'game/objects/general/waterfall.c', 'game/objects/setup.c', 'game/objects/traps/damocles_sword.c', @@ -244,11 +238,18 @@ sources = [ 'game/option/option_passport.c', 'game/option/option_sound.c', 'game/output.c', + 'game/output/draw_misc.c', + 'game/output/func.c', + 'game/output/meshes/common.c', + 'game/output/meshes/dynamic.c', + 'game/output/meshes/objects.c', + 'game/output/meshes/rooms.c', + 'game/output/shader.c', 'game/output/sprites.c', - 'game/output/sprite_program.c', + 'game/output/state.c', 'game/output/textures.c', + 'game/output/vertex_range.c', 'game/overlay.c', - 'game/requester.c', 'game/room.c', 'game/room_draw.c', 'game/savegame/savegame.c', @@ -261,12 +262,10 @@ sources = [ 'game/stats/common.c', 'game/text.c', 'game/ui/common.c', - 'game/ui/widgets/stats_dialog.c', - 'game/ui/widgets/paginator.c', + 'game/ui/dialogs/stats.c', 'game/viewport.c', 'global/enum_map.c', 'global/vars.c', - 'specific/s_output.c', 'specific/s_shell.c', resources, ] diff --git a/src/tr1/specific/s_output.c b/src/tr1/specific/s_output.c deleted file mode 100644 index f91e35a3d..000000000 --- a/src/tr1/specific/s_output.c +++ /dev/null @@ -1,1194 +0,0 @@ -#include "specific/s_output.h" - -#include "game/output.h" -#include "game/screen.h" -#include "game/shell.h" -#include "game/viewport.h" -#include "global/vars.h" -#include "specific/s_shell.h" - -#include -#include -#include -#include -#include - -#include - -#define CLIP_VERTCOUNT_SCALE 4 -#define MAP_DEPTH(zv) (g_FltResZBuf - g_FltResZ * (1.0 / (double)(zv))) -#define VBUF_VISIBLE(a, b, c) \ - (((a).ys - (b).ys) * ((c).xs - (b).xs) \ - >= ((c).ys - (b).ys) * ((a).xs - (b).xs)) - -#define S_Output_CheckError(result) \ - { \ - if (!result) { \ - Shell_ExitSystem("Fatal 2D renderer error!"); \ - } \ - } - -static int m_TextureMap[GFX_MAX_TEXTURES] = { GFX_NO_TEXTURE }; -static int m_EnvMapTexture = GFX_NO_TEXTURE; - -static GFX_2D_RENDERER *m_Renderer2D = nullptr; -static GFX_3D_RENDERER *m_Renderer3D = nullptr; -static bool m_IsTextureMode = false; -static int32_t m_SelectedTexture = -1; - -static int32_t m_SurfaceWidth = 0; -static int32_t m_SurfaceHeight = 0; -static float m_SurfaceMinX = 0.0f; -static float m_SurfaceMinY = 0.0f; -static float m_SurfaceMaxX = 0.0f; -static float m_SurfaceMaxY = 0.0f; -static GFX_2D_SURFACE *m_PrimarySurface = nullptr; -static GFX_2D_SURFACE *m_PictureSurface = nullptr; -static GFX_2D_SURFACE *m_TextureSurfaces[GFX_MAX_TEXTURES] = { nullptr }; - -static inline float M_GetUV(uint16_t uv); -static void M_ReleaseTextures(void); -static void M_ReleaseSurfaces(void); -static void M_FlipPrimaryBuffer(void); -static void M_ClearSurface(GFX_2D_SURFACE *surface); -static void M_DrawTriangleFan(GFX_3D_VERTEX *vertices, int vertex_count); -static void M_DrawTriangleStrip(GFX_3D_VERTEX *vertices, int vertex_count); -static int32_t M_VisibleZClip( - const PHD_VBUF *vn1, const PHD_VBUF *vn2, const PHD_VBUF *vn3); -static int32_t M_ZedClipper( - int32_t vertex_count, const PHD_VBUF *vns[], GFX_3D_VERTEX *vertices); - -static inline float M_GetUV(const uint16_t uv) -{ - return g_Config.rendering.pretty_pixels - && g_Config.rendering.texture_filter == GFX_TF_NN - ? uv / 65536.0f - : ((uv & 0xFF00) + 127) / 65536.0f; -} - -static void M_ReleaseTextures(void) -{ - if (m_Renderer3D == nullptr) { - return; - } - for (int i = 0; i < GFX_MAX_TEXTURES; i++) { - if (m_TextureMap[i] != GFX_NO_TEXTURE) { - GFX_3D_Renderer_UnregisterTexturePage( - m_Renderer3D, m_TextureMap[i]); - m_TextureMap[i] = GFX_NO_TEXTURE; - } - } - if (m_EnvMapTexture != GFX_NO_TEXTURE) { - GFX_3D_Renderer_UnregisterEnvironmentMap(m_Renderer3D, m_EnvMapTexture); - } -} - -static void M_ReleaseSurfaces(void) -{ - if (m_PrimarySurface) { - M_ClearSurface(m_PrimarySurface); - - GFX_2D_Surface_Free(m_PrimarySurface); - m_PrimarySurface = nullptr; - } - - for (int i = 0; i < GFX_MAX_TEXTURES; i++) { - if (m_TextureSurfaces[i] != nullptr) { - GFX_2D_Surface_Free(m_TextureSurfaces[i]); - m_TextureSurfaces[i] = nullptr; - } - } - - if (m_PictureSurface) { - GFX_2D_Surface_Free(m_PictureSurface); - m_PictureSurface = nullptr; - } -} - -void Output_FillEnvironmentMap(void) -{ - GFX_3D_Renderer_FillEnvironmentMap(m_Renderer3D); -} - -static void M_FlipPrimaryBuffer(void) -{ - GFX_Context_SwapBuffers(); -} - -static void M_ClearSurface(GFX_2D_SURFACE *surface) -{ - GFX_2D_Surface_Clear(surface, 0); -} - -static void M_DrawTriangleFan(GFX_3D_VERTEX *vertices, int vertex_count) -{ - GFX_3D_Renderer_RenderPrimFan(m_Renderer3D, vertices, vertex_count); -} - -static void M_DrawTriangleStrip(GFX_3D_VERTEX *vertices, int vertex_count) -{ - GFX_3D_Renderer_RenderPrimStrip(m_Renderer3D, vertices, vertex_count); -} - -static int32_t M_VisibleZClip( - const PHD_VBUF *const vn1, const PHD_VBUF *const vn2, - const PHD_VBUF *const vn3) -{ - double v1x = vn1->xv; - double v1y = vn1->yv; - double v1z = vn1->zv; - double v2x = vn2->xv; - double v2y = vn2->yv; - double v2z = vn2->zv; - double v3x = vn3->xv; - double v3y = vn3->yv; - double v3z = vn3->zv; - double a = v3y * v1x - v1y * v3x; - double b = v3x * v1z - v1x * v3z; - double c = v3z * v1y - v1z * v3y; - return a * v2z + b * v2y + c * v2x < 0.0; -} - -static int32_t M_ZedClipper( - const int32_t vertex_count, const PHD_VBUF *vns[], - GFX_3D_VERTEX *const vertices) -{ - const float multiplier = g_Config.visuals.brightness / 16.0f; - const float near_z = Output_GetNearZ(); - const float persp_o_near_z = (double)g_PhdPersp / near_z; - - GFX_3D_VERTEX *v = &vertices[0]; - int32_t current = 0; - int32_t prev = vertex_count - 1; - for (int32_t i = 0; i < vertex_count; i++) { - const PHD_VBUF *vn0 = vns[current]; - const PHD_VBUF *vn1 = vns[prev]; - const int32_t diff0 = near_z - vn0->zv; - const int32_t diff1 = near_z - vn1->zv; - if ((diff0 | diff1) >= 0) { - goto loop_end; - } - - if ((diff0 ^ diff1) < 0) { - const double clip = diff0 / (vn1->zv - vn0->zv); - v->x = (vn0->xv + (vn1->xv - vn0->xv) * clip) * persp_o_near_z - + Viewport_GetCenterX(); - v->y = (vn0->yv + (vn1->yv - vn0->yv) * clip) * persp_o_near_z - + Viewport_GetCenterY(); - v->z = MAP_DEPTH(vn0->zv + (vn1->zv - vn0->zv) * clip); - - v->w = 1.0f / near_z; - v->s = M_GetUV(vn0->u) + (M_GetUV(vn1->u) - M_GetUV(vn0->u)) * clip; - v->t = M_GetUV(vn0->v) + (M_GetUV(vn1->v) - M_GetUV(vn0->v)) * clip; - v->tex_coord[2] = vn0->tex_coord[2] - + (vn1->tex_coord[2] - vn0->tex_coord[2]) * clip; - v->tex_coord[3] = vn0->tex_coord[3] - + (vn1->tex_coord[3] - vn0->tex_coord[3]) * clip; - - v->r = v->g = v->b = - (8192.0f - (vn0->g + (vn1->g - vn0->g) * clip)) * multiplier; - Output_ApplyTint(&v->r, &v->g, &v->b); - - v++; - } - - if (diff0 < 0) { - v->x = vn0->xs; - v->y = vn0->ys; - v->z = MAP_DEPTH(vn0->zv); - - v->w = 1.0f / vn0->zv; - v->s = M_GetUV(vn0->u); - v->t = M_GetUV(vn0->v); - v->tex_coord[2] = vn0->tex_coord[2]; - v->tex_coord[3] = vn0->tex_coord[3]; - - v->r = v->g = v->b = (8192.0f - vn0->g) * multiplier; - Output_ApplyTint(&v->r, &v->g, &v->b); - - v++; - } - - loop_end: - prev = current++; - } - - const int32_t count = v - vertices; - return count < 3 ? 0 : count; -} - -void S_Output_EnableTextureMode(void) -{ - if (m_IsTextureMode) { - return; - } - - m_IsTextureMode = true; - GFX_3D_Renderer_SetTexturingEnabled(m_Renderer3D, m_IsTextureMode); -} - -void S_Output_DisableTextureMode(void) -{ - if (!m_IsTextureMode) { - return; - } - - m_IsTextureMode = false; - GFX_3D_Renderer_SetTexturingEnabled(m_Renderer3D, m_IsTextureMode); -} - -void S_Output_SetBlendingMode(const GFX_BLEND_MODE blend_mode) -{ - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, blend_mode); -} - -void S_Output_EnableDepthWrites(void) -{ - GFX_3D_Renderer_SetDepthWritesEnabled(m_Renderer3D, true); -} - -void S_Output_DisableDepthWrites(void) -{ - GFX_3D_Renderer_SetDepthWritesEnabled(m_Renderer3D, false); -} - -void S_Output_EnableDepthTest(void) -{ - GFX_3D_Renderer_SetDepthTestEnabled(m_Renderer3D, true); -} - -void S_Output_DisableDepthTest(void) -{ - GFX_3D_Renderer_SetDepthTestEnabled(m_Renderer3D, false); -} - -void S_Output_RenderBegin(void) -{ - GFX_Context_Clear(); - GFX_Track_Reset(); - S_Output_DrawBackdropSurface(); - GFX_3D_Renderer_RenderBegin(m_Renderer3D); - GFX_3D_Renderer_SetTextureFilter( - m_Renderer3D, g_Config.rendering.texture_filter); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); -} - -void S_Output_RenderEnd(void) -{ - GFX_3D_Renderer_RenderEnd(m_Renderer3D); -} - -void S_Output_Flush(void) -{ - GFX_3D_Renderer_Flush(m_Renderer3D); -} - -void S_Output_FlipScreen(void) -{ - M_FlipPrimaryBuffer(); - m_SelectedTexture = -1; -} - -void S_Output_ClearDepthBuffer(void) -{ - GFX_3D_Renderer_ClearDepth(m_Renderer3D); -} - -void S_Output_DrawBackdropSurface(void) -{ - if (m_PictureSurface == nullptr) { - return; - } - GFX_2D_Renderer_Render(m_Renderer2D); -} - -void S_Output_DownloadBackdropSurface(const IMAGE *const image) -{ - GFX_2D_Surface_Free(m_PictureSurface); - m_PictureSurface = nullptr; - - if (image == nullptr) { - return; - } - - m_PictureSurface = GFX_2D_Surface_CreateFromImage(image); - GFX_2D_Renderer_Upload( - m_Renderer2D, &m_PictureSurface->desc, m_PictureSurface->buffer); -} - -void S_Output_SelectTexture(const int32_t texture_num) -{ - if (texture_num == m_SelectedTexture) { - return; - } - - if (m_TextureMap[texture_num] == GFX_NO_TEXTURE) { - LOG_ERROR("ERROR: Attempt to select unloaded texture"); - return; - } - - GFX_3D_Renderer_SelectTexture(m_Renderer3D, m_TextureMap[texture_num]); - - m_SelectedTexture = texture_num; -} - -void S_Output_DrawSprite( - int16_t x1, int16_t y1, int16_t x2, int y2, int z, int sprnum, int shade) -{ - int vertex_count = 4; - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - - float multiplier = g_Config.visuals.brightness / 16.0f; - - const SPRITE_TEXTURE *const sprite = Output_GetSpriteTexture(sprnum); - float vshade = (8192.0f - shade) * multiplier; - if (vshade >= 256.0f) { - vshade = 255.0f; - } - - const float u0 = (sprite->offset & 0xFF) / 256.0f; - const float v0 = (sprite->offset >> 8) / 256.0f; - const float u1 = (sprite->width >> 8) / 256.0f + u0; - const float v1 = (sprite->height >> 8) / 256.0f + v0; - const float vz = MAP_DEPTH(z); - const float rhw = 1.0f / z; - - vertices[0].x = x1; - vertices[0].y = y1; - vertices[0].z = vz; - vertices[0].s = u0; - vertices[0].t = v0; - vertices[0].tex_coord[2] = 1.0; - vertices[0].tex_coord[3] = 1.0; - vertices[0].w = rhw; - vertices[0].r = vshade; - vertices[0].g = vshade; - vertices[0].b = vshade; - - vertices[1].x = x2; - vertices[1].y = y1; - vertices[1].z = vz; - vertices[1].s = u1; - vertices[1].t = v0; - vertices[1].tex_coord[2] = 1.0; - vertices[1].tex_coord[3] = 1.0; - vertices[1].w = rhw; - vertices[1].r = vshade; - vertices[1].g = vshade; - vertices[1].b = vshade; - - vertices[2].x = x2; - vertices[2].y = y2; - vertices[2].z = vz; - vertices[2].s = u1; - vertices[2].t = v1; - vertices[2].tex_coord[2] = 1.0; - vertices[2].tex_coord[3] = 1.0; - vertices[2].w = rhw; - vertices[2].r = vshade; - vertices[2].g = vshade; - vertices[2].b = vshade; - - vertices[3].x = x1; - vertices[3].y = y2; - vertices[3].z = vz; - vertices[3].s = u0; - vertices[3].t = v1; - vertices[3].tex_coord[2] = 1.0; - vertices[3].tex_coord[3] = 1.0; - vertices[3].w = rhw; - vertices[3].r = vshade; - vertices[3].g = vshade; - vertices[3].b = vshade; - - if (!vertex_count) { - return; - } - - if (m_TextureMap[sprite->tex_page] != GFX_NO_TEXTURE) { - S_Output_EnableTextureMode(); - S_Output_SelectTexture(sprite->tex_page); - M_DrawTriangleFan(vertices, vertex_count); - } else { - S_Output_DisableTextureMode(); - M_DrawTriangleFan(vertices, vertex_count); - } -} - -void S_Output_Draw3DLine( - const PHD_VBUF *const vn0, const PHD_VBUF *const vn1, const RGBA_8888 color) -{ - int32_t vertex_count = 2; - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - const PHD_VBUF *const src_vbuf[2] = { vn0, vn1 }; - - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].x = src_vbuf[i]->xs; - vertices[i].y = src_vbuf[i]->ys; - vertices[i].z = MAP_DEPTH(src_vbuf[i]->zv); - vertices[i].r = color.r; - vertices[i].g = color.g; - vertices[i].b = color.b; - vertices[i].a = color.a; - } - - if ((vn0->clip | vn1->clip) < 0) { - vertex_count = - M_ZedClipper(vertex_count, (const PHD_VBUF **)src_vbuf, vertices); - if (vertex_count == 0) { - return; - } - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].r = color.r; - vertices[i].g = color.g; - vertices[i].b = color.b; - vertices[i].a = color.a; - } - } - - if (!vertex_count) { - return; - } - - GFX_3D_Renderer_SetPrimType(m_Renderer3D, GFX_3D_PRIM_LINE); - S_Output_DisableTextureMode(); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_NORMAL); - GFX_3D_Renderer_RenderPrimList(m_Renderer3D, vertices, vertex_count); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); - GFX_3D_Renderer_SetPrimType(m_Renderer3D, GFX_3D_PRIM_TRI); -} - -void S_Output_Draw2DQuad( - int32_t x1, int32_t y1, int32_t x2, int32_t y2, RGBA_8888 tl, RGBA_8888 tr, - RGBA_8888 bl, RGBA_8888 br) -{ - int vertex_count = 4; - GFX_3D_VERTEX vertices[vertex_count]; - - vertices[0].x = x1; - vertices[0].y = y1; - vertices[0].z = 1.0f; - vertices[0].r = tl.r; - vertices[0].g = tl.g; - vertices[0].b = tl.b; - vertices[0].a = tl.a; - - vertices[1].x = x2; - vertices[1].y = y1; - vertices[1].z = 1.0f; - vertices[1].r = tr.r; - vertices[1].g = tr.g; - vertices[1].b = tr.b; - vertices[1].a = tr.a; - - vertices[2].x = x2; - vertices[2].y = y2; - vertices[2].z = 1.0f; - vertices[2].r = br.r; - vertices[2].g = br.g; - vertices[2].b = br.b; - vertices[2].a = br.a; - - vertices[3].x = x1; - vertices[3].y = y2; - vertices[3].z = 1.0f; - vertices[3].r = bl.r; - vertices[3].g = bl.g; - vertices[3].b = bl.b; - vertices[3].a = bl.a; - - S_Output_DisableTextureMode(); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_NORMAL); - M_DrawTriangleFan(vertices, vertex_count); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); -} - -void S_Output_DrawLightningSegment( - int x1, int y1, int z1, int thickness1, int x2, int y2, int z2, - int thickness2) -{ - int vertex_count = 4; - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - - S_Output_DisableTextureMode(); - - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_NORMAL); - vertices[0].x = x1; - vertices[0].y = y1; - vertices[0].z = MAP_DEPTH(z1); - vertices[0].g = 0.0f; - vertices[0].r = 0.0f; - vertices[0].b = 255.0f; - vertices[0].a = 128.0f; - - vertices[1].x = thickness1 / 2 + x1; - vertices[1].y = vertices[0].y; - vertices[1].z = vertices[0].z; - vertices[1].b = 255.0f; - vertices[1].g = 255.0f; - vertices[1].r = 255.0f; - vertices[1].a = 128.0f; - - vertices[2].x = thickness2 / 2 + x2; - vertices[2].y = y2; - vertices[2].z = MAP_DEPTH(z2); - vertices[2].b = 255.0f; - vertices[2].g = 255.0f; - vertices[2].r = 255.0f; - vertices[2].a = 128.0f; - - vertices[3].x = x2; - vertices[3].y = vertices[2].y; - vertices[3].z = vertices[2].z; - vertices[3].g = 0.0f; - vertices[3].r = 0.0f; - vertices[3].b = 255.0f; - vertices[3].a = 128.0f; - - M_DrawTriangleFan(vertices, vertex_count); - - vertex_count = 4; - vertices[0].x = thickness1 / 2 + x1; - vertices[0].y = y1; - vertices[0].z = MAP_DEPTH(z1); - vertices[0].b = 255.0f; - vertices[0].g = 255.0f; - vertices[0].r = 255.0f; - vertices[0].a = 128.0f; - - vertices[1].x = thickness1 + x1; - vertices[1].y = vertices[0].y; - vertices[1].z = vertices[0].z; - vertices[1].g = 0.0f; - vertices[1].r = 0.0f; - vertices[1].b = 255.0f; - vertices[1].a = 128.0f; - - vertices[2].x = (thickness2 + x2); - vertices[2].y = y2; - vertices[2].z = MAP_DEPTH(z2); - vertices[2].g = 0.0f; - vertices[2].r = 0.0f; - vertices[2].b = 255.0f; - vertices[2].a = 128.0f; - - vertices[3].x = (thickness2 / 2 + x2); - vertices[3].y = vertices[2].y; - vertices[3].z = vertices[2].z; - vertices[3].b = 255.0f; - vertices[3].g = 255.0f; - vertices[3].r = 255.0f; - vertices[3].a = 128.0f; - - M_DrawTriangleFan(vertices, vertex_count); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); -} - -void S_Output_DrawShadow(PHD_VBUF *vbufs, int clip, int vertex_count) -{ - // needs to be more than 8 cause clipping might return more polygons. - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - - for (int32_t i = 0; i < vertex_count; i++) { - GFX_3D_VERTEX *vertex = &vertices[i]; - PHD_VBUF *vbuf = &vbufs[i]; - vertex->x = vbuf->xs; - vertex->y = vbuf->ys; - vertex->z = MAP_DEPTH(vbuf->zv - (12 << W2V_SHIFT)); - vertex->b = 0.0f; - vertex->g = 0.0f; - vertex->r = 0.0f; - vertex->a = 128.0f; - } - - if (vertex_count == 0) { - return; - } - - S_Output_DisableTextureMode(); - - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_NORMAL); - M_DrawTriangleFan(vertices, vertex_count); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); -} - -void S_Output_ApplyRenderSettings(void) -{ - if (m_Renderer3D == nullptr) { - return; - } - - if (m_PictureSurface != nullptr - && (Screen_GetResWidth() != m_SurfaceWidth - || Screen_GetResHeight() != m_SurfaceHeight)) { - GFX_2D_Surface_Free(m_PictureSurface); - m_PictureSurface = nullptr; - } - - m_SurfaceWidth = Screen_GetResWidth(); - m_SurfaceHeight = Screen_GetResHeight(); - m_SurfaceMinX = 0.0f; - m_SurfaceMinY = 0.0f; - m_SurfaceMaxX = Screen_GetResWidth() - 1.0f; - m_SurfaceMaxY = Screen_GetResHeight() - 1.0f; - - GFX_Context_SetVSync(g_Config.rendering.enable_vsync); - GFX_Context_SetDisplayFilter(g_Config.rendering.fbo_filter); - GFX_Context_SetDisplaySize(m_SurfaceWidth, m_SurfaceHeight); - GFX_Context_SetRenderingMode(g_Config.rendering.render_mode); - GFX_Context_SetWireframeMode(g_Config.rendering.enable_wireframe); - GFX_Context_SetLineWidth(g_Config.rendering.wireframe_width); - GFX_3D_Renderer_SetAnisotropyFilter( - m_Renderer3D, g_Config.rendering.anisotropy_filter); - - if (m_PrimarySurface == nullptr) { - GFX_2D_SURFACE_DESC surface_desc = {}; - m_PrimarySurface = GFX_2D_Surface_Create(&surface_desc); - } - M_ClearSurface(m_PrimarySurface); -} - -void S_Output_SetWindowSize(int width, int height) -{ - GFX_Context_SetWindowSize(width, height); -} - -bool S_Output_Init(void) -{ - for (int i = 0; i < GFX_MAX_TEXTURES; i++) { - m_TextureMap[i] = GFX_NO_TEXTURE; - m_TextureSurfaces[i] = nullptr; - } - - m_Renderer2D = GFX_2D_Renderer_Create(); - m_Renderer3D = GFX_3D_Renderer_Create(); - - S_Output_ApplyRenderSettings(); - GFX_3D_Renderer_SetPrimType(m_Renderer3D, GFX_3D_PRIM_TRI); - GFX_3D_Renderer_SetAlphaThreshold(m_Renderer3D, 0.0); - GFX_3D_Renderer_SetAlphaPointDiscard(m_Renderer3D, true); - - return true; -} - -void S_Output_Shutdown(void) -{ - M_ReleaseTextures(); - M_ReleaseSurfaces(); - - if (m_Renderer2D != nullptr) { - GFX_2D_Renderer_Destroy(m_Renderer2D); - m_Renderer2D = nullptr; - } - if (m_Renderer3D != nullptr) { - GFX_3D_Renderer_Destroy(m_Renderer3D); - m_Renderer3D = nullptr; - } - GFX_Context_Detach(); -} - -void S_Output_DrawFlatTriangle( - PHD_VBUF *vn1, PHD_VBUF *vn2, PHD_VBUF *vn3, RGBA_8888 color) -{ - int vertex_count = 3; - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - PHD_VBUF *src_vbuf[3]; - - src_vbuf[0] = vn1; - src_vbuf[1] = vn2; - src_vbuf[2] = vn3; - - if (vn3->clip & vn2->clip & vn1->clip) { - return; - } - - float multiplier = g_Config.visuals.brightness / (16.0f * 255.0f); - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].x = src_vbuf[i]->xs; - vertices[i].y = src_vbuf[i]->ys; - vertices[i].z = MAP_DEPTH(src_vbuf[i]->zv); - const float light = (8192.0f - src_vbuf[i]->g) * multiplier; - vertices[i].r = color.r * light; - vertices[i].g = color.g * light; - vertices[i].b = color.b * light; - vertices[i].a = color.a; - - Output_ApplyTint(&vertices[i].r, &vertices[i].g, &vertices[i].b); - } - - if ((vn1->clip | vn2->clip | vn3->clip) >= 0) { - if (!VBUF_VISIBLE(*vn1, *vn2, *vn3)) { - return; - } - } else { - if (!M_VisibleZClip(vn1, vn2, vn3)) { - return; - } - - vertex_count = - M_ZedClipper(vertex_count, (const PHD_VBUF **)src_vbuf, vertices); - if (vertex_count == 0) { - return; - } - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].r *= color.r / 255.0f; - vertices[i].g *= color.g / 255.0f; - vertices[i].b *= color.b / 255.0f; - vertices[i].a = color.a; - } - } - - if (!vertex_count) { - return; - } - - M_DrawTriangleFan(vertices, vertex_count); -} - -void S_Output_DrawEnvMapTriangle( - const PHD_VBUF *const vn1, const PHD_VBUF *const vn2, - const PHD_VBUF *const vn3) -{ - int vertex_count = 3; - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - - const float multiplier = g_Config.visuals.brightness / 16.0f; - const PHD_VBUF *const src_vbuf[3] = { vn1, vn2, vn3 }; - - if (vn3->clip & vn2->clip & vn1->clip) { - return; - } - - if (vn1->clip >= 0 && vn2->clip >= 0 && vn3->clip >= 0) { - if (!VBUF_VISIBLE(*vn1, *vn2, *vn3)) { - return; - } - - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].x = src_vbuf[i]->xs; - vertices[i].y = src_vbuf[i]->ys; - vertices[i].z = MAP_DEPTH(src_vbuf[i]->zv); - - vertices[i].w = 1.0f / src_vbuf[i]->zv; - vertices[i].s = M_GetUV(src_vbuf[i]->u); - vertices[i].t = M_GetUV(src_vbuf[i]->v); - vertices[i].tex_coord[2] = src_vbuf[i]->tex_coord[2]; - vertices[i].tex_coord[3] = src_vbuf[i]->tex_coord[3]; - - vertices[i].r = vertices[i].g = vertices[i].b = - (8192.0f - src_vbuf[i]->g) * multiplier; - - Output_ApplyTint(&vertices[i].r, &vertices[i].g, &vertices[i].b); - } - } else { - if (!M_VisibleZClip(vn1, vn2, vn3)) { - return; - } - - vertex_count = - M_ZedClipper(vertex_count, (const PHD_VBUF **)src_vbuf, vertices); - if (vertex_count == 0) { - return; - } - } - - if (!vertex_count) { - return; - } - - S_Output_EnableTextureMode(); - GFX_3D_Renderer_SelectTexture(m_Renderer3D, m_EnvMapTexture); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_MULTIPLY); - M_DrawTriangleFan(vertices, vertex_count); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); - m_SelectedTexture = -1; -} - -void S_Output_DrawEnvMapQuad( - const PHD_VBUF *const vn1, const PHD_VBUF *const vn2, - const PHD_VBUF *const vn3, const PHD_VBUF *const vn4) -{ - int vertex_count = 4; - GFX_3D_VERTEX vertices[vertex_count]; - - if (vn4->clip | vn3->clip | vn2->clip | vn1->clip) { - if ((vn4->clip & vn3->clip & vn2->clip & vn1->clip)) { - return; - } - - if (vn1->clip >= 0 && vn2->clip >= 0 && vn3->clip >= 0 - && vn4->clip >= 0) { - if (!VBUF_VISIBLE(*vn1, *vn2, *vn3)) { - return; - } - } else if (!M_VisibleZClip(vn1, vn2, vn3)) { - return; - } - - S_Output_DrawEnvMapTriangle(vn1, vn2, vn3); - S_Output_DrawEnvMapTriangle(vn3, vn4, vn1); - return; - } - - if (!VBUF_VISIBLE(*vn1, *vn2, *vn3)) { - return; - } - - float multiplier = g_Config.visuals.brightness / 16.0f; - - const PHD_VBUF *const src_vbuf[4] = { vn2, vn1, vn3, vn4 }; - - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].x = src_vbuf[i]->xs; - vertices[i].y = src_vbuf[i]->ys; - vertices[i].z = MAP_DEPTH(src_vbuf[i]->zv); - - vertices[i].w = 1.0f / src_vbuf[i]->zv; - vertices[i].s = M_GetUV(src_vbuf[i]->u); - vertices[i].t = M_GetUV(src_vbuf[i]->v); - vertices[i].tex_coord[2] = src_vbuf[i]->tex_coord[2]; - vertices[i].tex_coord[3] = src_vbuf[i]->tex_coord[3]; - - vertices[i].r = vertices[i].g = vertices[i].b = - (8192.0f - src_vbuf[i]->g) * multiplier; - - Output_ApplyTint(&vertices[i].r, &vertices[i].g, &vertices[i].b); - } - - S_Output_EnableTextureMode(); - GFX_3D_Renderer_SelectTexture(m_Renderer3D, m_EnvMapTexture); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_MULTIPLY); - GFX_3D_Renderer_RenderPrimStrip(m_Renderer3D, vertices, vertex_count); - GFX_3D_Renderer_SetBlendingMode(m_Renderer3D, GFX_BLEND_MODE_OFF); - m_SelectedTexture = -1; -} - -void S_Output_DrawTexturedTriangle( - PHD_VBUF *vn1, PHD_VBUF *vn2, PHD_VBUF *vn3, int16_t tpage, - uint16_t textype) -{ - int vertex_count = 3; - GFX_3D_VERTEX vertices[vertex_count * CLIP_VERTCOUNT_SCALE]; - PHD_VBUF *src_vbuf[3]; - - float multiplier = g_Config.visuals.brightness / 16.0f; - - src_vbuf[0] = vn1; - src_vbuf[1] = vn2; - src_vbuf[2] = vn3; - - if (vn3->clip & vn2->clip & vn1->clip) { - return; - } - - if (src_vbuf[0]->clip >= 0 && src_vbuf[1]->clip >= 0 - && src_vbuf[2]->clip >= 0) { - if (!VBUF_VISIBLE(*src_vbuf[0], *src_vbuf[1], *src_vbuf[2])) { - return; - } - - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].x = src_vbuf[i]->xs; - vertices[i].y = src_vbuf[i]->ys; - vertices[i].z = MAP_DEPTH(src_vbuf[i]->zv); - - vertices[i].w = 1.0f / src_vbuf[i]->zv; - vertices[i].s = M_GetUV(src_vbuf[i]->u); - vertices[i].t = M_GetUV(src_vbuf[i]->v); - vertices[i].tex_coord[2] = src_vbuf[i]->tex_coord[2]; - vertices[i].tex_coord[3] = src_vbuf[i]->tex_coord[3]; - - vertices[i].r = vertices[i].g = vertices[i].b = - (8192.0f - src_vbuf[i]->g) * multiplier; - - Output_ApplyTint(&vertices[i].r, &vertices[i].g, &vertices[i].b); - } - } else { - if (!M_VisibleZClip(src_vbuf[0], src_vbuf[1], src_vbuf[2])) { - return; - } - - vertex_count = - M_ZedClipper(vertex_count, (const PHD_VBUF **)src_vbuf, vertices); - if (vertex_count == 0) { - return; - } - } - - if (!vertex_count) { - return; - } - - if (m_TextureMap[tpage] != GFX_NO_TEXTURE) { - S_Output_EnableTextureMode(); - S_Output_SelectTexture(tpage); - M_DrawTriangleFan(vertices, vertex_count); - } else { - S_Output_DisableTextureMode(); - M_DrawTriangleFan(vertices, vertex_count); - } -} - -void S_Output_DrawTexturedQuad( - PHD_VBUF *vn1, PHD_VBUF *vn2, PHD_VBUF *vn3, PHD_VBUF *vn4, int16_t tpage, - uint16_t textype) -{ - int vertex_count = 4; - GFX_3D_VERTEX vertices[vertex_count]; - PHD_VBUF *src_vbuf[4] = { vn1, vn2, vn3, vn4 }; - - if (src_vbuf[3]->clip | src_vbuf[2]->clip | src_vbuf[1]->clip - | src_vbuf[0]->clip) { - if ((src_vbuf[3]->clip & src_vbuf[2]->clip & src_vbuf[1]->clip - & src_vbuf[0]->clip)) { - return; - } - - if (src_vbuf[0]->clip >= 0 && src_vbuf[1]->clip >= 0 - && src_vbuf[2]->clip >= 0 && src_vbuf[3]->clip >= 0) { - if (!VBUF_VISIBLE(*src_vbuf[0], *src_vbuf[1], *src_vbuf[2])) { - return; - } - } else if (!M_VisibleZClip(src_vbuf[0], src_vbuf[1], src_vbuf[2])) { - return; - } - - S_Output_DrawTexturedTriangle(vn1, vn2, vn3, tpage, textype); - S_Output_DrawTexturedTriangle(vn3, vn4, vn1, tpage, textype); - return; - } - - if (!VBUF_VISIBLE(*src_vbuf[0], *src_vbuf[1], *src_vbuf[2])) { - return; - } - - float multiplier = g_Config.visuals.brightness / 16.0f; - - for (int32_t i = 0; i < vertex_count; i++) { - vertices[i].x = src_vbuf[i]->xs; - vertices[i].y = src_vbuf[i]->ys; - vertices[i].z = MAP_DEPTH(src_vbuf[i]->zv); - - vertices[i].w = 1.0f / src_vbuf[i]->zv; - vertices[i].s = M_GetUV(src_vbuf[i]->u); - vertices[i].t = M_GetUV(src_vbuf[i]->v); - vertices[i].tex_coord[2] = src_vbuf[i]->tex_coord[2]; - vertices[i].tex_coord[3] = src_vbuf[i]->tex_coord[3]; - - vertices[i].r = vertices[i].g = vertices[i].b = - (8192.0f - src_vbuf[i]->g) * multiplier; - - Output_ApplyTint(&vertices[i].r, &vertices[i].g, &vertices[i].b); - } - - if (m_TextureMap[tpage] != GFX_NO_TEXTURE) { - S_Output_EnableTextureMode(); - S_Output_SelectTexture(tpage); - } else { - S_Output_DisableTextureMode(); - } - - GFX_3D_Renderer_RenderPrimFan(m_Renderer3D, vertices, vertex_count); -} - -void S_Output_DownloadTextures(int32_t pages) -{ - if (pages > GFX_MAX_TEXTURES) { - Shell_ExitSystem("Attempt to download more than texture page limit"); - } - - M_ReleaseTextures(); - - for (int32_t i = 0; i < pages; i++) { - if (m_TextureSurfaces[i] == nullptr) { - const GFX_2D_SURFACE_DESC surface_desc = { - .width = TEXTURE_PAGE_WIDTH, - .height = TEXTURE_PAGE_HEIGHT, - }; - m_TextureSurfaces[i] = GFX_2D_Surface_Create(&surface_desc); - } - GFX_2D_SURFACE *const surface = m_TextureSurfaces[i]; - RGBA_8888 *const output_ptr = (RGBA_8888 *)surface->buffer; - const RGBA_8888 *const input_ptr = Output_GetTexturePage32(i); - memcpy( - output_ptr, input_ptr, - surface->desc.width * surface->desc.height * sizeof(RGBA_8888)); - - m_TextureMap[i] = GFX_3D_Renderer_RegisterTexturePage( - m_Renderer3D, output_ptr, surface->desc.width, - surface->desc.height); - } - - m_SelectedTexture = -1; - - m_EnvMapTexture = GFX_3D_Renderer_RegisterEnvironmentMap(m_Renderer3D); -} - -void S_Output_DrawScreenFrame( - const int32_t sx, const int32_t sy, const int32_t w, const int32_t h, - const RGBA_8888 col_dark, const RGBA_8888 col_light, const float thickness) -{ -#define SB_NUM_VERTS_DARK 12 -#define SB_NUM_VERTS_LIGHT 10 - GFX_3D_VERTEX vertices[SB_NUM_VERTS_DARK + SB_NUM_VERTS_LIGHT]; - GFX_3D_VERTEX *const dark_vertices = vertices; - GFX_3D_VERTEX *const light_vertices = vertices + SB_NUM_VERTS_DARK; - const float sxf = sx + thickness; - const float syf = sy + thickness; - const float hf = h; - const float wf = w; - -#define SET(i, x_, y_) \ - vertices[i].x = x_; \ - vertices[i].y = y_; - // clang-format off - // Top Left Dark edge - SET(0, sxf, syf + hf - thickness); - SET(1, sxf + thickness, syf + hf - thickness); - SET(2, sxf, syf); - SET(3, sxf + thickness, syf + thickness); - SET(4, sxf + wf - thickness, syf); - SET(5, sxf + wf - thickness, syf + thickness); - // Bottom Right Dark set - SET(6, sxf + wf + thickness, syf - thickness); - SET(7, sxf + wf, syf - thickness); - SET(8, sxf + wf + thickness, syf + hf + thickness); - SET(9, sxf + wf, syf + hf); - SET(10, sxf - thickness, syf + hf + thickness); - SET(11, sxf - thickness, syf + hf); - // Light box - SET(12, sxf - thickness, syf + hf); - SET(13, sxf - thickness + thickness, syf + hf - thickness); - SET(14, sxf - thickness, syf - thickness); - SET(15, sxf, syf); - SET(16, sxf + wf, syf - thickness); - SET(17, sxf + wf - thickness, syf); - SET(18, sxf + wf, syf + hf); - SET(19, sxf + wf - thickness, syf + hf - thickness); - SET(20, sxf - thickness, syf + hf); - SET(21, sxf - thickness + thickness, syf + hf - thickness); - // clang-format on -#undef SET - - for (int32_t i = 0; i < SB_NUM_VERTS_DARK + SB_NUM_VERTS_LIGHT; i++) { - vertices[i].z = 1.0f; - vertices[i].s = 0.0f; - vertices[i].t = 0.0f; - vertices[i].w = 0.0f; - } - for (int32_t i = 0; i < SB_NUM_VERTS_DARK; i++) { - dark_vertices[i].r = col_dark.r; - dark_vertices[i].g = col_dark.g; - dark_vertices[i].b = col_dark.b; - dark_vertices[i].a = col_dark.a; - } - for (int32_t i = 0; i < SB_NUM_VERTS_LIGHT; i++) { - light_vertices[i].r = col_light.r; - light_vertices[i].g = col_light.g; - light_vertices[i].b = col_light.b; - light_vertices[i].a = col_light.a; - } - S_Output_DisableTextureMode(); - M_DrawTriangleStrip(vertices, SB_NUM_VERTS_DARK + SB_NUM_VERTS_LIGHT); -} - -void S_Output_4ColourTextBox( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 tl, RGBA_8888 tr, - RGBA_8888 bl, RGBA_8888 br, float thickness) -{ - // 0 2 - // * & - // 1 3 - // - // 7 5 - // # @ - // 6 4 - GFX_3D_VERTEX vertices[10]; - -#define SET(i, x_, y_, color) \ - vertices[i].x = x_; \ - vertices[i].y = y_; \ - vertices[i].r = color.r; \ - vertices[i].g = color.g; \ - vertices[i].b = color.b; \ - vertices[i].a = color.a; - // clang-format off - SET(0, sx - thickness, sy - thickness, tl); - SET(1, sx + thickness, sy + thickness, tl); - SET(2, sx + w + thickness, sy - thickness, tr); - SET(3, sx + w - thickness, sy + thickness, tr); - SET(4, sx + w + thickness, sy + h + thickness, br); - SET(5, sx + w - thickness, sy + h - thickness, br); - SET(6, sx - thickness, sy + h + thickness, bl); - SET(7, sx + thickness, sy + h - thickness, bl); - SET(8, sx - thickness, sy - thickness, tl); - SET(9, sx + thickness, sy + thickness, tl); - // clang-format on -#undef SET - - for (int32_t i = 0; i < 10; i++) { - vertices[i].z = 1.0f; - vertices[i].s = 0.0f; - vertices[i].t = 0.0f; - vertices[i].w = 0.0f; - } - S_Output_DisableTextureMode(); - M_DrawTriangleStrip(vertices, 10); -} - -void S_Output_2ToneColourTextBox( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 edge, - RGBA_8888 centre, float thickness) -{ - // 0 2 4 - // * & - // 1 3 5 - // - // 14 15 7 6 - // - // 13 10 9 - // # @ - // 12 11 8 - - int32_t half_w = w / 2; - int32_t half_h = h / 2; - GFX_3D_VERTEX vertices[18]; - -#define SET(i, x_, y_, color) \ - vertices[i].x = x_; \ - vertices[i].y = y_; \ - vertices[i].r = color.r; \ - vertices[i].g = color.g; \ - vertices[i].b = color.b; \ - vertices[i].a = color.a; - // clang-format off - SET(0, sx - thickness, sy - thickness, edge); - SET(1, sx + thickness, sy + thickness, edge); - SET(2, sx + half_w, sy - thickness, centre); - SET(3, sx + half_w, sy + thickness, centre); - SET(4, sx + w + thickness, sy - thickness, edge); - SET(5, sx + w - thickness, sy + thickness, edge); - SET(6, sx + w + thickness, sy + half_h, centre); - SET(7, sx + w - thickness, sy + half_h, centre); - SET(8, sx + w + thickness, sy + h + thickness, edge); - SET(9, sx + w - thickness, sy + h - thickness, edge); - SET(10, sx + half_w, sy + h + thickness, centre); - SET(11, sx + half_w, sy + h - thickness, centre); - SET(12, sx - thickness, sy + h + thickness, edge); - SET(13, sx + thickness, sy + h - thickness, edge); - SET(14, sx - thickness, sy + half_h, centre); - SET(15, sx + thickness, sy + half_h, centre); - SET(16, sx - thickness, sy - thickness, edge); - SET(17, sx + thickness, sy + thickness, edge); - // clang-format on -#undef SET - - for (int32_t i = 0; i < 18; i++) { - vertices[i].z = 1.0f; - vertices[i].s = 0.0f; - vertices[i].t = 0.0f; - vertices[i].w = 0.0f; - } - S_Output_DisableTextureMode(); - M_DrawTriangleStrip(vertices, 18); -} diff --git a/src/tr1/specific/s_output.h b/src/tr1/specific/s_output.h deleted file mode 100644 index fb4c483f0..000000000 --- a/src/tr1/specific/s_output.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include "global/types.h" - -#include -#include - -#include - -bool S_Output_Init(void); -void S_Output_Shutdown(void); - -void S_Output_EnableTextureMode(void); -void S_Output_DisableTextureMode(void); -void S_Output_SetBlendingMode(GFX_BLEND_MODE blend_mode); -void S_Output_EnableDepthWrites(void); -void S_Output_DisableDepthWrites(void); -void S_Output_EnableDepthTest(void); -void S_Output_DisableDepthTest(void); - -void S_Output_RenderBegin(void); -void S_Output_RenderEnd(void); -void S_Output_Flush(void); -void S_Output_FlipScreen(void); -void S_Output_ClearDepthBuffer(void); - -void S_Output_SetWindowSize(int width, int height); -void S_Output_ApplyRenderSettings(void); - -void S_Output_DownloadTextures(int32_t pages); -void S_Output_SelectTexture(int32_t texture_num); -void S_Output_DownloadBackdropSurface(const IMAGE *image); -void S_Output_DrawBackdropSurface(void); - -void S_Output_DrawFlatTriangle( - PHD_VBUF *vn1, PHD_VBUF *vn2, PHD_VBUF *vn3, RGBA_8888 color); -void S_Output_DrawEnvMapTriangle( - const PHD_VBUF *vn1, const PHD_VBUF *vn2, const PHD_VBUF *vn3); -void S_Output_DrawEnvMapQuad( - const PHD_VBUF *vn1, const PHD_VBUF *vn2, const PHD_VBUF *vn3, - const PHD_VBUF *vn4); -void S_Output_DrawTexturedTriangle( - PHD_VBUF *vn1, PHD_VBUF *vn2, PHD_VBUF *vn3, int16_t tpage, - uint16_t textype); -void S_Output_DrawTexturedQuad( - PHD_VBUF *vn1, PHD_VBUF *vn2, PHD_VBUF *vn3, PHD_VBUF *vn4, int16_t tpage, - uint16_t textype); -void S_Output_Draw3DLine( - const PHD_VBUF *vn1, const PHD_VBUF *vn2, const RGBA_8888 color); -void S_Output_DrawSprite( - int16_t x1, int16_t y1, int16_t x2, int y2, int z, int sprnum, int shade); -void S_Output_Draw2DQuad( - int32_t x1, int32_t y1, int32_t x2, int32_t y2, RGBA_8888 tl, RGBA_8888 tr, - RGBA_8888 bl, RGBA_8888 br); -void S_Output_DrawShadow(PHD_VBUF *vbufs, int clip, int vertex_count); -void S_Output_DrawLightningSegment( - int x1, int y1, int z1, int thickness1, int x2, int y2, int z2, - int thickness2); - -void S_Output_DrawScreenFrame( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 col_dark, - RGBA_8888 col_light, float thickness); -void S_Output_4ColourTextBox( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 tl, RGBA_8888 tr, - RGBA_8888 bl, RGBA_8888 br, float thickness); -void S_Output_2ToneColourTextBox( - int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 edge, - RGBA_8888 centre, float thickness); diff --git a/src/tr1/specific/s_shell.c b/src/tr1/specific/s_shell.c index 8a9cb9372..0bf5fd1f2 100644 --- a/src/tr1/specific/s_shell.c +++ b/src/tr1/specific/s_shell.c @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include @@ -129,8 +129,6 @@ void S_Shell_HandleWindowResize(void) M_SetWindowPos(x, y, false); M_SetWindowSize(width, height, false); - UI_Events_Fire(&(EVENT) { .name = "canvas_resize" }); - // save the updated config, but ensure it was loaded first if (g_Config.loaded) { Config_Write(); diff --git a/src/tr2/decomp/decomp.c b/src/tr2/decomp/decomp.c index 2b2a3e2b7..ef658fcec 100644 --- a/src/tr2/decomp/decomp.c +++ b/src/tr2/decomp/decomp.c @@ -11,7 +11,6 @@ #include "game/objects/vars.h" #include "game/output.h" #include "game/phase.h" -#include "game/requester.h" #include "game/room.h" #include "game/viewport.h" #include "global/vars.h" @@ -144,29 +143,13 @@ void DecreaseScreenSize(void) } } -void GetValidLevelsList(REQUEST_INFO *const req) -{ - Requester_RemoveAllItems(req); - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); - for (int32_t i = 0; i < level_table->count; i++) { - const GF_LEVEL *const level = &level_table->levels[i]; - if (level->type != GFL_GYM) { - Requester_AddItem(req, level->title, 0, nullptr, 0); - } - } -} - void InitialiseGameFlags(void) { Music_ResetTrackFlags(); - for (GAME_OBJECT_ID obj_id = 0; obj_id < O_NUMBER_OF; obj_id++) { - Object_Get(obj_id)->loaded = 0; - } - Output_SetSunsetTimer(0); g_LevelComplete = false; g_DetonateAllMines = false; - g_IsMonkAngry = false; + Creature_SetAlliesHostile(false); } void GetCarriedItems(void) diff --git a/src/tr2/decomp/decomp.h b/src/tr2/decomp/decomp.h index dad2724a3..b5f872622 100644 --- a/src/tr2/decomp/decomp.h +++ b/src/tr2/decomp/decomp.h @@ -19,7 +19,6 @@ void S_InitialisePolyList(bool clear_back_buffer); void DecreaseScreenSize(void); void IncreaseScreenSize(void); void S_UnloadLevelFile(void); -void GetValidLevelsList(REQUEST_INFO *req); void InitialiseGameFlags(void); void GetCarriedItems(void); int32_t DoShift(ITEM *vehicle, const XYZ_32 *pos, const XYZ_32 *old); diff --git a/src/tr2/decomp/flares.c b/src/tr2/decomp/flares.c index 42cb861c5..616f24c70 100644 --- a/src/tr2/decomp/flares.c +++ b/src/tr2/decomp/flares.c @@ -12,6 +12,7 @@ #include "global/vars.h" #include +#include #include #include #include @@ -82,7 +83,7 @@ int32_t Flare_DoLight(const XYZ_32 *const pos, const int32_t flare_age) const int32_t random = Random_GetDraw(); const XYZ_32 light_pos = { - .x = pos->x + (random & 0xF), + .x = pos->x + (random & 0xA0), .y = pos->y, .z = pos->z, }; @@ -142,8 +143,22 @@ void Flare_DrawInAir(const ITEM *const item) Matrix_TranslateAbs32(item->interp.result.pos); Matrix_Rot16(item->interp.result.rot); const int32_t clip = Output_GetObjectBounds(&frames[0]->bounds); + + const XYZ_32 flare_size = { + .x = frames[0]->bounds.max.x - frames[0]->bounds.min.x, + .y = frames[0]->bounds.max.y - frames[0]->bounds.min.y, + .z = frames[0]->bounds.max.z - frames[0]->bounds.min.z, + }; + const XYZ_32 flare_offset = { + .x = -flare_size.x, + .y = -flare_size.y, + .z = -flare_size.z, + }; + Matrix_TranslateRel32(flare_offset); + if (clip != 0) { Output_CalculateObjectLighting(item, &frames[0]->bounds); + Output_SetDepthBias(-20); Object_DrawMesh(Object_Get(O_FLARE_ITEM)->mesh_idx, clip, false); if (((int32_t)(intptr_t)item->data) & 0x8000) { Matrix_TranslateRel(-6, 6, 80); @@ -152,6 +167,7 @@ void Flare_DrawInAir(const ITEM *const item) Output_CalculateStaticLight(8 * 256); Object_DrawMesh(Object_Get(O_FLARE_FIRE)->mesh_idx, clip, false); } + Output_SetDepthBias(0); } Matrix_Pop(); } @@ -258,7 +274,7 @@ void Flare_Draw(void) frame_num = LF_FL_DRAW; } else if (frame_num == LF_FL_DRAW_GOT_IT) { Flare_DrawMeshes(); - if (!g_SaveGame.bonus_flag) { + if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { Inv_RemoveItem(O_FLARES_ITEM); } } else if (frame_num >= LF_FL_IGNITE && frame_num <= LF_FL_2_HOLD - 2) { diff --git a/src/tr2/decomp/savegame.h b/src/tr2/decomp/savegame.h deleted file mode 100644 index 513af70a2..000000000 --- a/src/tr2/decomp/savegame.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "game/game_flow/types.h" -#include "global/types.h" - -#include - -#include - -void Savegame_Init(void); -void Savegame_Shutdown(void); - -void Savegame_InitCurrentInfo(void); - -void Savegame_ResetCurrentInfo(const GF_LEVEL *level); -START_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level); -void Savegame_CarryCurrentInfoToNextLevel( - const GF_LEVEL *src_level, const GF_LEVEL *dst_level); -void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level); -void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level); -void CreateSaveGameInfo(void); -void ExtractSaveGameInfo(void); - -void GetSavedGamesList(REQUEST_INFO *req); -bool S_FrontEndCheck(void); -bool S_SaveGame(int32_t slot_num); -bool S_LoadGame(int32_t slot_num); - -void Savegame_SetDefaultStats(const GF_LEVEL *level, STATS_COMMON stats); -STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *level); diff --git a/src/tr2/game/camera.c b/src/tr2/game/camera.c index d29e32f9f..93ce34c17 100644 --- a/src/tr2/game/camera.c +++ b/src/tr2/game/camera.c @@ -877,6 +877,12 @@ void Camera_LoadCutsceneFrame(void) g_Camera.roll = roll; g_Camera.shift = 0; + const int16_t room_num = + Room_FindByPos(g_Camera.pos.x, g_Camera.pos.y, g_Camera.pos.z); + if (room_num != NO_ROOM_NEG) { + g_Camera.pos.room_num = room_num; + } + Viewport_AlterFOV(fov); if (g_Config.audio.enable_lara_mic) { diff --git a/src/tr2/game/creature.c b/src/tr2/game/creature.c index 7e314c54b..015943e29 100644 --- a/src/tr2/game/creature.c +++ b/src/tr2/game/creature.c @@ -1,938 +1,24 @@ #include "game/creature.h" #include "game/box.h" -#include "game/camera.h" -#include "game/effects.h" #include "game/gun/gun_misc.h" -#include "game/items.h" -#include "game/lara/misc.h" #include "game/los.h" -#include "game/lot.h" -#include "game/objects/common.h" -#include "game/objects/vars.h" #include "game/random.h" -#include "game/room.h" #include "game/spawn.h" -#include "global/const.h" -#include "global/vars.h" #include +#include #include #include -#define ESCAPE_CHANCE 2048 -#define RECOVER_CHANCE 256 -#define MAX_X_ROT (20 * DEG_1) // = 3640 -#define MAX_TILT (3 * DEG_1) // = 546 -#define MAX_HEAD_CHANGE (5 * DEG_1) // = 910 -#define HEAD_ARC 0x3000 // = 12288 -#define FLOAT_SPEED 32 -#define TARGET_TOLERANCE 0x400000 - -#define CREATURE_ATTACK_RANGE SQUARE(WALL_L * 3) // = 0x900000 = 9437184 -#define CREATURE_SHOOT_TARGETING_SPEED 300 -#define CREATURE_SHOOT_RANGE SQUARE(WALL_L * 8) // = 0x4000000 = 67108864 -#define CREATURE_SHOOT_HIT_CHANCE 0x2000 - -void Creature_Initialise(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - item->rot.y += (Random_GetControl() - DEG_90) >> 1; - item->collidable = 1; - item->data = 0; -} - -void Creature_AIInfo(ITEM *const item, AI_INFO *const info) -{ - CREATURE *const creature = (CREATURE *)item->data; - if (creature == nullptr) { - return; - } - - switch (item->object_id) { - case O_BANDIT_1: - case O_BANDIT_2: - Creature_GetBaddieTarget(creature->item_num, false); - break; - - case O_MONK_1: - case O_MONK_2: - Creature_GetBaddieTarget(creature->item_num, true); - break; - - default: - creature->enemy = g_LaraItem; - break; - } - - ITEM *enemy = creature->enemy; - if (enemy == nullptr) { - enemy = g_LaraItem; - } - - const int16_t *const zone = Box_GetLotZone(&creature->lot); - - { - const ROOM *const room = Room_Get(item->room_num); - item->box_num = - Room_GetWorldSector(room, item->pos.x, item->pos.z)->box; - info->zone_num = zone[item->box_num]; - } - - { - const ROOM *const room = Room_Get(enemy->room_num); - enemy->box_num = - Room_GetWorldSector(room, enemy->pos.x, enemy->pos.z)->box; - info->enemy_zone_num = zone[enemy->box_num]; - } - - if (((Box_GetBox(enemy->box_num)->overlap_index & creature->lot.block_mask) - != 0) - || (creature->lot.node[item->box_num].search_num - == (creature->lot.search_num | BOX_BLOCKED_SEARCH))) { - info->enemy_zone_num |= BOX_BLOCKED; - } - - const OBJECT *const obj = Object_Get(item->object_id); - const int32_t z = enemy->pos.z - - ((obj->pivot_length * Math_Cos(item->rot.y)) >> W2V_SHIFT) - - item->pos.z; - const int32_t x = enemy->pos.x - - ((obj->pivot_length * Math_Sin(item->rot.y)) >> W2V_SHIFT) - - item->pos.x; - int16_t angle = Math_Atan(z, x); - if (creature->enemy != nullptr) { - info->distance = SQUARE(x) + SQUARE(z); - } else { - info->distance = 0x7FFFFFFF; - } - - info->angle = angle - item->rot.y; - info->enemy_facing = angle - enemy->rot.y + DEG_180; - info->ahead = info->angle > -FRONT_ARC && info->angle < FRONT_ARC; - info->bite = info->ahead && enemy->hit_points > 0 - && ABS(enemy->pos.y - item->pos.y) <= STEP_L; -} - -void Creature_Mood( - const ITEM *const item, const AI_INFO *const info, const bool violent) -{ - CREATURE *const creature = item->data; - if (creature == nullptr) { - return; - } - - LOT_INFO *const lot = &creature->lot; - const ITEM *enemy = creature->enemy; - if (lot->node[item->box_num].search_num - == (lot->search_num | BOX_BLOCKED_SEARCH)) { - lot->required_box = NO_BOX; - } - if (creature->mood != MOOD_ATTACK && lot->required_box != NO_BOX - && !Box_ValidBox(item, info->zone_num, lot->target_box)) { - if (info->zone_num == info->enemy_zone_num) { - creature->mood = MOOD_BORED; - } - lot->required_box = NO_BOX; - } - - const MOOD_TYPE mood = creature->mood; - if (enemy == nullptr) { - creature->mood = MOOD_BORED; - enemy = g_LaraItem; - } else if (enemy->hit_points <= 0) { - creature->mood = MOOD_BORED; - } else if (violent) { - switch (mood) { - case MOOD_BORED: - case MOOD_STALK: - if (info->zone_num == info->enemy_zone_num) { - creature->mood = MOOD_ATTACK; - } else if (item->hit_status) { - creature->mood = MOOD_ESCAPE; - } - break; - - case MOOD_ATTACK: - if (info->zone_num != info->enemy_zone_num) { - creature->mood = MOOD_BORED; - } - break; - - case MOOD_ESCAPE: - if (info->zone_num == info->enemy_zone_num) { - creature->mood = MOOD_ATTACK; - } - break; - } - } else { - switch (mood) { - case MOOD_BORED: - case MOOD_STALK: - if (item->hit_status - && (Random_GetControl() < ESCAPE_CHANCE - || info->zone_num != info->enemy_zone_num)) { - creature->mood = MOOD_ESCAPE; - } else if (info->zone_num == info->enemy_zone_num) { - if (info->distance < CREATURE_ATTACK_RANGE - || (creature->mood == MOOD_STALK - && lot->required_box == NO_BOX)) { - creature->mood = MOOD_ATTACK; - } else { - creature->mood = MOOD_STALK; - } - } - break; - - case MOOD_ATTACK: - if (item->hit_status - && (Random_GetControl() < ESCAPE_CHANCE - || info->zone_num != info->enemy_zone_num)) { - creature->mood = MOOD_ESCAPE; - } else if (info->zone_num != info->enemy_zone_num) { - creature->mood = MOOD_BORED; - } - break; - - case MOOD_ESCAPE: - if (info->zone_num == info->enemy_zone_num - && Random_GetControl() < RECOVER_CHANCE) { - creature->mood = MOOD_STALK; - } - break; - } - } - - if (mood != creature->mood) { - if (mood == MOOD_ATTACK) { - Box_TargetBox(lot, lot->target_box); - } - lot->required_box = NO_BOX; - } - - switch (creature->mood) { - case MOOD_BORED: { - const int16_t box_num = - lot->node[(lot->zone_count * Random_GetControl()) >> 15].box_num; - if (Box_ValidBox(item, info->zone_num, box_num)) { - if (Box_StalkBox(item, enemy, box_num) && creature->enemy != nullptr - && enemy->hit_points > 0) { - Box_TargetBox(lot, box_num); - creature->mood = MOOD_STALK; - } else if (lot->required_box == NO_BOX) { - Box_TargetBox(lot, box_num); - } - } - break; - } - - case MOOD_ATTACK: - lot->target = enemy->pos; - lot->required_box = enemy->box_num; - if (lot->fly != 0 && g_Lara.water_status == LWS_ABOVE_WATER) { - lot->target.y += Item_GetBestFrame(enemy)->bounds.min.y; - } - break; - - case MOOD_ESCAPE: { - const int16_t box_num = - lot->node[(lot->zone_count * Random_GetControl()) >> 15].box_num; - if (Box_ValidBox(item, info->zone_num, box_num) - && lot->required_box == NO_BOX) { - if (Box_EscapeBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - } else if ( - info->zone_num == info->enemy_zone_num - && Box_StalkBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - creature->mood = MOOD_STALK; - } - } - break; - } - - case MOOD_STALK: { - if (lot->required_box == NO_BOX - || !Box_StalkBox(item, enemy, lot->required_box)) { - const int16_t box_num = - lot->node[(lot->zone_count * Random_GetControl()) >> 15] - .box_num; - if (Box_ValidBox(item, info->zone_num, box_num)) { - if (Box_StalkBox(item, enemy, box_num)) { - Box_TargetBox(lot, box_num); - } else if (lot->required_box == NO_BOX) { - Box_TargetBox(lot, box_num); - if (info->zone_num != info->enemy_zone_num) { - creature->mood = MOOD_BORED; - } - } - } - } - break; - } - } - - if (lot->target_box == NO_BOX) { - Box_TargetBox(lot, item->box_num); - } - Box_CalculateTarget(&creature->target, item, lot); -} - -int32_t Creature_CheckBaddieOverlap(const int16_t item_num) -{ - const ITEM *item = Item_Get(item_num); - - const int32_t x = item->pos.x; - const int32_t y = item->pos.y; - const int32_t z = item->pos.z; - const int32_t radius = SQUARE(Object_Get(item->object_id)->radius); - - int16_t link = Room_Get(item->room_num)->item_num; - while (link != NO_ITEM && link != item_num) { - item = Item_Get(link); - if (item != g_LaraItem && item->status == IS_ACTIVE - && item->speed != 0) { - const int32_t distance = - (SQUARE(item->pos.z - z) + SQUARE(item->pos.y - y) - + SQUARE(item->pos.x - x)); - if (distance < radius) { - return true; - } - } - - link = item->next_item; - } - - return false; -} - -void Creature_Die(const int16_t item_num, const bool explode) -{ - ITEM *const item = Item_Get(item_num); - - if (item->object_id == O_DRAGON_FRONT) { - item->hit_points = 0; - return; - } - - if (item->object_id == O_SKIDOO_DRIVER) { - if (explode) { - Item_Explode(item_num, -1, 0); - } - item->hit_points = DONT_TARGET; - const int16_t vehicle_item_num = (int16_t)(intptr_t)item->data; - ITEM *const vehicle_item = Item_Get(vehicle_item_num); - vehicle_item->hit_points = 0; - return; - } - - item->collidable = 0; - item->hit_points = DONT_TARGET; - if (explode) { - Item_Explode(item_num, -1, 0); - Item_Kill(item_num); - } else { - Item_RemoveActive(item_num); - } - - const OBJECT *const obj = Object_Get(item->object_id); - if (obj->intelligent) { - LOT_DisableBaddieAI(item_num); - } - item->flags |= IF_ONE_SHOT; - - if (item->killed) { - item->next_active = Item_GetPrevActive(); - Item_SetPrevActive(item_num); - } - - if (obj->intelligent) { - int16_t pickup_num = item->carried_item; - while (pickup_num != NO_ITEM) { - ITEM *const pickup = Item_Get(pickup_num); - pickup->pos = item->pos; - Item_NewRoom(pickup_num, item->room_num); - pickup_num = pickup->carried_item; - } - } -} - -bool Creature_Animate( - const int16_t item_num, const int16_t angle, const int16_t tilt) -{ - ITEM *const item = Item_Get(item_num); - const CREATURE *const creature = item->data; - const OBJECT *const obj = Object_Get(item->object_id); - if (creature == nullptr) { - return false; - } - - const LOT_INFO *const lot = &creature->lot; - const XYZ_32 old = item->pos; - - const int16_t *const zone = Box_GetLotZone(lot); - - if (!Object_IsType(item->object_id, g_WaterObjects)) { - int16_t room_num = item->room_num; - Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); - if (room_num != item->room_num) { - Item_NewRoom(item_num, room_num); - } - } - - Item_Animate(item); - if (item->status == IS_DEACTIVATED) { - Creature_Die(item_num, false); - return false; - } - - const BOUNDS_16 *const bounds = Item_GetBoundsAccurate(item); - int32_t y = item->pos.y + bounds->min.y; - - int16_t room_num = item->room_num; - Room_GetSector(old.x, y, old.z, &room_num); - const SECTOR *sector = - Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - int32_t height = Box_GetBox(sector->box)->height; - int16_t next_box = lot->node[sector->box].exit_box; - int32_t next_height = - next_box != NO_BOX ? Box_GetBox(next_box)->height : height; - - const int32_t box_height = Box_GetBox(item->box_num)->height; - if (sector->box == NO_BOX || zone[item->box_num] != zone[sector->box] - || box_height - height > lot->step || box_height - height < lot->drop) { - const int32_t pos_x = item->pos.x >> WALL_SHIFT; - const int32_t shift_x = old.x >> WALL_SHIFT; - const int32_t shift_z = old.z >> WALL_SHIFT; - - if (pos_x < shift_x) { - item->pos.x = old.x & (~(WALL_L - 1)); - } else if (pos_x > shift_x) { - item->pos.x = old.x | (WALL_L - 1); - } - - if (pos_x < shift_z) { - item->pos.z = old.z & (~(WALL_L - 1)); - } else if (pos_x > shift_z) { - item->pos.z = old.z | (WALL_L - 1); - } - - sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - height = Box_GetBox(sector->box)->height; - next_box = lot->node[sector->box].exit_box; - next_height = - next_box != NO_BOX ? Box_GetBox(next_box)->height : height; - } - - const int32_t x = item->pos.x; - const int32_t z = item->pos.z; - const int32_t pos_x = x & (WALL_L - 1); - const int32_t pos_z = z & (WALL_L - 1); - int32_t shift_x = 0; - int32_t shift_z = 0; - const int32_t radius = obj->radius; - - if (pos_z < radius) { - if (Box_BadFloor( - x, y, z - radius, height, next_height, room_num, lot)) { - shift_z = radius - pos_z; - } - - if (pos_x < radius) { - if (Box_BadFloor( - x - radius, y, z, height, next_height, room_num, lot)) { - shift_x = radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x - radius, y, z - radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { - shift_z = radius - pos_z; - } else { - shift_x = radius - pos_x; - } - } - } else if (pos_x > WALL_L - radius) { - if (Box_BadFloor( - x + radius, y, z, height, next_height, room_num, lot)) { - shift_x = WALL_L - radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x + radius, y, z - radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { - shift_z = radius - pos_z; - } else { - shift_x = WALL_L - radius - pos_x; - } - } - } - } else if (pos_z > WALL_L - radius) { - if (Box_BadFloor( - x, y, z + radius, height, next_height, room_num, lot)) { - shift_z = WALL_L - radius - pos_z; - } - - if (pos_x < radius) { - if (Box_BadFloor( - x - radius, y, z, height, next_height, room_num, lot)) { - shift_x = radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x - radius, y, z + radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_45 && item->rot.y < DEG_135) { - shift_x = radius - pos_x; - } else { - shift_z = WALL_L - radius - pos_z; - } - } - } else if (pos_x > WALL_L - radius) { - if (Box_BadFloor( - x + radius, y, z, height, next_height, room_num, lot)) { - shift_x = WALL_L - radius - pos_x; - } else if ( - !shift_z - && Box_BadFloor( - x + radius, y, z + radius, height, next_height, room_num, - lot)) { - if (item->rot.y > -DEG_135 && item->rot.y < DEG_45) { - shift_x = WALL_L - radius - pos_x; - } else { - shift_z = WALL_L - radius - pos_z; - } - } - } - } else if (pos_x < radius) { - if (Box_BadFloor( - x - radius, y, z, height, next_height, room_num, lot)) { - shift_x = radius - pos_x; - } - } else if (pos_x > WALL_L - radius) { - if (Box_BadFloor( - x + radius, y, z, height, next_height, room_num, lot)) { - shift_x = WALL_L - radius - pos_x; - } - } - - item->pos.x += shift_x; - item->pos.z += shift_z; - - if (shift_x || shift_z) { - sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - item->rot.y += angle; - if (tilt != 0) { - Creature_Tilt(item, tilt * 2); - } - } - - if (Creature_CheckBaddieOverlap(item_num)) { - item->pos = old; - return true; - } - - if (lot->fly) { - int32_t dy = creature->target.y - item->pos.y; - CLAMP(dy, -lot->fly, lot->fly); - - height = Room_GetHeight(sector, item->pos.x, y, item->pos.z); - if (item->pos.y + dy <= height) { - const int32_t ceiling = - Room_GetCeiling(sector, item->pos.x, y, item->pos.z); - const int32_t min_y = - item->object_id == O_SHARK ? 128 : bounds->min.y; - if (item->pos.y + min_y + dy < ceiling) { - if (item->pos.y + min_y < ceiling) { - item->pos.x = old.x; - item->pos.z = old.z; - dy = lot->fly; - } else { - dy = 0; - } - } - } else if (item->pos.y <= height) { - item->pos.y = height; - dy = 0; - } else { - item->pos.x = old.x; - item->pos.z = old.z; - dy = -lot->fly; - } - - item->pos.y += dy; - sector = Room_GetSector(item->pos.x, y, item->pos.z, &room_num); - item->floor = Room_GetHeight(sector, item->pos.x, y, item->pos.z); - - int16_t angle = item->speed != 0 ? Math_Atan(item->speed, -dy) : 0; - CLAMP(angle, -MAX_X_ROT, MAX_X_ROT); - - if (angle < item->rot.x - DEG_1) { - item->rot.x -= DEG_1; - } else if (angle > item->rot.x + DEG_1) { - item->rot.x += DEG_1; - } else { - item->rot.x = angle; - } - } else { - const SECTOR *const sector = - Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); - item->floor = - Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); - - if (item->pos.y > item->floor) { - item->pos.y = item->floor; - } else if (item->floor - item->pos.y > STEP_L / 4) { - item->pos.y += STEP_L / 4; - } else if (item->pos.y < item->floor) { - item->pos.y = item->floor; - } - item->rot.x = 0; - } - - if (!Object_IsType(item->object_id, g_WaterObjects)) { - Room_GetSector( - item->pos.x, item->pos.y - (STEP_L * 2), item->pos.z, &room_num); - if (Room_Get(room_num)->flags & RF_UNDERWATER) { - item->hit_points = 0; - } - } - - if (item->room_num != room_num) { - Item_NewRoom(item_num, room_num); - } - return true; -} - -int16_t Creature_Turn(ITEM *const item, int16_t max_turn) -{ - const CREATURE *const creature = item->data; - if (creature == nullptr || item->speed == 0 || max_turn == 0) { - return 0; - } - - const int32_t dx = creature->target.x - item->pos.x; - const int32_t dz = creature->target.z - item->pos.z; - - int16_t angle = Math_Atan(dz, dx) - item->rot.y; - if (angle > FRONT_ARC || angle < -FRONT_ARC) { - const int32_t range = (item->speed << 14) / max_turn; - if (SQUARE(dx) + SQUARE(dz) < SQUARE(range)) { - max_turn /= 2; - } - } - - CLAMP(angle, -max_turn, max_turn); - item->rot.y += angle; - return angle; -} - -void Creature_Tilt(ITEM *const item, int16_t angle) -{ - angle = 4 * angle - item->rot.z; - CLAMP(angle, -MAX_TILT, MAX_TILT); - item->rot.z += angle; -} - -void Creature_Head(ITEM *item, int16_t required) -{ - CREATURE *const creature = item->data; - if (creature == nullptr) { - return; - } - - int16_t change = required - creature->head_rotation; - CLAMP(change, -MAX_HEAD_CHANGE, MAX_HEAD_CHANGE); - - creature->head_rotation += change; - CLAMP(creature->head_rotation, -HEAD_ARC, HEAD_ARC); -} - -void Creature_Neck(ITEM *const item, const int16_t required) -{ - CREATURE *const creature = item->data; - if (creature == nullptr) { - return; - } - - int16_t change = required - creature->neck_rotation; - CLAMP(change, -MAX_HEAD_CHANGE, MAX_HEAD_CHANGE); - - creature->neck_rotation += change; - CLAMP(creature->neck_rotation, -HEAD_ARC, HEAD_ARC); -} - -void Creature_Float(const int16_t item_num) -{ - ITEM *const item = Item_Get(item_num); - - item->hit_points = DONT_TARGET; - item->rot.x = 0; - - const int32_t wh = Room_GetWaterHeight( - item->pos.x, item->pos.y, item->pos.z, item->room_num); - if (item->pos.y > wh) { - item->pos.y -= FLOAT_SPEED; - } else if (item->pos.y < wh) { - item->pos.y = wh; - } - - Item_Animate(item); - - int16_t room_num = item->room_num; - const SECTOR *const sector = - Room_GetSector(item->pos.x, item->pos.y, item->pos.z, &room_num); - item->floor = Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z); - if (room_num != item->room_num) { - Item_NewRoom(item_num, room_num); - } -} - -void Creature_Underwater(ITEM *const item, const int32_t depth) -{ - const int32_t wh = Room_GetWaterHeight( - item->pos.x, item->pos.y, item->pos.z, item->room_num); - if (item->pos.y >= wh + depth) { - return; - } - - item->pos.y = wh + depth; - if (item->rot.x > 2 * DEG_1) { - item->rot.x -= 2 * DEG_1; - } else { - CLAMPG(item->rot.x, 0); - } -} - -int16_t Creature_Effect( - const ITEM *const item, const BITE *const bite, - int16_t (*const spawn)( - int32_t x, int32_t y, int32_t z, int16_t speed, int16_t y_rot, - int16_t room_num)) -{ - XYZ_32 pos = bite->pos; - Collide_GetJointAbsPosition(item, &pos, bite->mesh_num); - return (*spawn)( - pos.x, pos.y, pos.z, item->speed, item->rot.y, item->room_num); -} - -int32_t Creature_Vault( - const int16_t item_num, const int16_t angle, int32_t vault, - const int32_t shift) -{ - ITEM *const item = Item_Get(item_num); - const int16_t room_num = item->room_num; - const XYZ_32 old = item->pos; - - Creature_Animate(item_num, angle, 0); - - if (item->floor > old.y + STEP_L * 7 / 2) { - vault = -4; - } else if (item->pos.y > old.y - STEP_L * 3 / 2) { - return 0; - } else if (item->pos.y > old.y - STEP_L * 5 / 2) { - vault = 2; - } else if (item->pos.y > old.y - STEP_L * 7 / 2) { - vault = 3; - } else { - vault = 4; - } - - const int32_t old_x_sector = old.x >> WALL_SHIFT; - const int32_t old_z_sector = old.z >> WALL_SHIFT; - const int32_t x_sector = item->pos.x >> WALL_SHIFT; - const int32_t z_sector = item->pos.z >> WALL_SHIFT; - if (old_z_sector == z_sector) { - if (old_x_sector == x_sector) { - return 0; - } - - if (old_x_sector >= x_sector) { - item->rot.y = -DEG_90; - item->pos.x = (old_x_sector << WALL_SHIFT) + shift; - } else { - item->rot.y = DEG_90; - item->pos.x = (x_sector << WALL_SHIFT) - shift; - } - } else if (old_x_sector == x_sector) { - if (old_z_sector >= z_sector) { - item->rot.y = -DEG_180; - item->pos.z = (old_z_sector << WALL_SHIFT) + shift; - } else { - item->rot.y = 0; - item->pos.z = (z_sector << WALL_SHIFT) - shift; - } - } - - item->floor = old.y; - item->pos.y = old.y; - - if (room_num != item->room_num) { - Item_NewRoom(item_num, room_num); - } - return vault; -} - -void Creature_Kill( - ITEM *const item, const int32_t kill_anim, const int32_t kill_state, - const int32_t lara_kill_state) -{ - Item_SwitchToAnim(item, kill_anim, 0); - item->current_anim_state = kill_state; - - Item_SwitchToObjAnim(g_LaraItem, LA_EXTRA_BREATH, 0, O_LARA_EXTRA); - g_LaraItem->current_anim_state = LA_EXTRA_BREATH; - g_LaraItem->goal_anim_state = lara_kill_state; - g_LaraItem->pos = item->pos; - g_LaraItem->rot = item->rot; - g_LaraItem->fall_speed = 0; - g_LaraItem->gravity = 0; - g_LaraItem->speed = 0; - - int16_t room_num = item->room_num; - if (room_num != g_LaraItem->room_num) { - Item_NewRoom(g_Lara.item_num, room_num); - } - - Item_Animate(g_LaraItem); - - g_LaraItem->goal_anim_state = lara_kill_state; - g_LaraItem->current_anim_state = lara_kill_state; - g_Lara.extra_anim = 1; - g_Lara.gun_status = LGS_HANDS_BUSY; - g_Lara.gun_type = LGT_UNARMED; - g_Lara.hit_direction = -1; - g_Lara.air = -1; - - g_Camera.pos.room_num = g_LaraItem->room_num; -} - -void Creature_GetBaddieTarget(const int16_t item_num, const int32_t goody) -{ - ITEM *const item = Item_Get(item_num); - CREATURE *const creature = item->data; - - ITEM *best_item = nullptr; - int32_t best_distance = INT32_MAX; - for (int32_t i = 0; i < NUM_SLOTS; i++) { - const int16_t target_item_num = g_BaddieSlots[i].item_num; - if (target_item_num == NO_ITEM || target_item_num == item_num) { - continue; - } - - ITEM *const target = Item_Get(target_item_num); - const GAME_OBJECT_ID obj_id = target->object_id; - if (goody && obj_id != O_BANDIT_1 && obj_id != O_BANDIT_2) { - continue; - } else if (!goody && obj_id != O_MONK_1 && obj_id != O_MONK_2) { - continue; - } - - const int32_t dx = (target->pos.x - item->pos.x) >> 6; - const int32_t dy = (target->pos.y - item->pos.y) >> 6; - const int32_t dz = (target->pos.z - item->pos.z) >> 6; - const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); - if (distance < best_distance) { - best_item = target; - best_distance = distance; - } - } - - if (best_item == nullptr) { - if (!goody || g_IsMonkAngry) { - creature->enemy = g_LaraItem; - } else { - creature->enemy = nullptr; - } - return; - } - - if (!goody || g_IsMonkAngry) { - const int32_t dx = (g_LaraItem->pos.x - item->pos.x) >> 6; - const int32_t dy = (g_LaraItem->pos.y - item->pos.y) >> 6; - const int32_t dz = (g_LaraItem->pos.z - item->pos.z) >> 6; - const int32_t distance = SQUARE(dx) + SQUARE(dy) + SQUARE(dz); - if (distance < best_distance) { - best_item = g_LaraItem; - best_distance = distance; - } - } - - const ITEM *const target = creature->enemy; - if (target == nullptr || target->status != IS_ACTIVE) { - creature->enemy = best_item; - } else { - const int32_t dx = (target->pos.x - item->pos.x) >> 6; - const int32_t dy = (target->pos.y - item->pos.y) >> 6; - const int32_t dz = (target->pos.z - item->pos.z) >> 6; - const int32_t distance = SQUARE(dz) + SQUARE(dy) + SQUARE(dx); - if (distance < best_distance + TARGET_TOLERANCE) { - creature->enemy = best_item; - } - } -} - -void Creature_Collision( - const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) -{ - ITEM *const item = Item_Get(item_num); - if (!Item_TestBoundsCollide(item, lara_item, coll->radius)) { - return; - } - - if (!Collide_TestCollision(item, lara_item)) { - return; - } - - if (coll->enable_baddie_push && g_Lara.water_status != LWS_UNDERWATER - && g_Lara.water_status != LWS_SURFACE) { - Lara_Push(item, lara_item, coll, coll->enable_hit, false); - } -} - -int32_t Creature_CanTargetEnemy( - const ITEM *const item, const AI_INFO *const info) -{ - const CREATURE *const creature = item->data; - const ITEM *const enemy = creature->enemy; - if (enemy->hit_points <= 0 || !info->ahead - || info->distance >= CREATURE_SHOOT_RANGE) { - return 0; - } - - GAME_VECTOR start; - start.pos.x = item->pos.x; - start.pos.y = item->pos.y - STEP_L * 3; - start.pos.z = item->pos.z; - start.room_num = item->room_num; - - GAME_VECTOR target; - target.pos.x = enemy->pos.x; - target.pos.y = enemy->pos.y - STEP_L * 3; - target.pos.z = enemy->pos.z; - target.room_num = enemy->room_num; - - return LOS_Check(&start, &target); -} - -bool Creature_IsHostile(const ITEM *const item) -{ - return Object_IsType(item->object_id, g_EnemyObjects) - || (g_IsMonkAngry - && (item->object_id == O_MONK_1 || item->object_id == O_MONK_2)); -} - -bool Creature_IsAlly(const ITEM *const item) -{ - return Object_IsType(item->object_id, g_AllyObjects); -} +#define M_SHOOT_TARGETING_SPEED 300 +#define M_SHOOT_HIT_CHANCE 0x2000 int32_t Creature_ShootAtLara( ITEM *const item, const AI_INFO *const info, const BITE *const gun, const int16_t extra_rotation, const int32_t damage) { + const ITEM *const lara_item = Lara_GetItem(); const CREATURE *const creature = item->data; ITEM *const target_item = creature->enemy; @@ -946,12 +32,12 @@ int32_t Creature_ShootAtLara( int32_t distance = (((target_item->speed * Math_Sin(info->enemy_facing)) >> W2V_SHIFT) * CREATURE_SHOOT_RANGE) - / CREATURE_SHOOT_TARGETING_SPEED; + / M_SHOOT_TARGETING_SPEED; distance = info->distance + SQUARE(distance); if (distance > CREATURE_SHOOT_RANGE) { is_hit = false; } else { - const int32_t chance = CREATURE_SHOOT_HIT_CHANCE + const int32_t chance = M_SHOOT_HIT_CHANCE + (CREATURE_SHOOT_RANGE - info->distance) / (CREATURE_SHOOT_RANGE / 0x5000); is_hit = Random_GetControl() < chance; @@ -960,7 +46,7 @@ int32_t Creature_ShootAtLara( } int16_t effect_num = NO_EFFECT; - if (target_item == g_LaraItem) { + if (target_item == lara_item) { if (is_hit) { effect_num = Creature_Effect(item, gun, Spawn_GunHit); Item_TakeDamage(target_item, damage, true); diff --git a/src/tr2/game/creature.h b/src/tr2/game/creature.h index d4d2a53b2..6aabac727 100644 --- a/src/tr2/game/creature.h +++ b/src/tr2/game/creature.h @@ -4,18 +4,6 @@ #include -int32_t Creature_CheckBaddieOverlap(int16_t item_num); -void Creature_Die(int16_t item_num, bool explode); -void Creature_Neck(ITEM *item, int16_t required); -void Creature_Float(int16_t item_num); -void Creature_Underwater(ITEM *item, int32_t depth); -int32_t Creature_Vault( - int16_t item_num, int16_t angle, int32_t vault, int32_t shift); -void Creature_Kill( - ITEM *item, int32_t kill_anim, int32_t kill_state, int32_t lara_kill_state); -void Creature_GetBaddieTarget(int16_t item_num, int32_t goody); -int32_t Creature_CanTargetEnemy(const ITEM *item, const AI_INFO *info); -bool Creature_IsAlly(const ITEM *item); int32_t Creature_ShootAtLara( ITEM *item, const AI_INFO *info, const BITE *gun, int16_t extra_rotation, int32_t damage); diff --git a/src/tr2/game/demo.c b/src/tr2/game/demo.c index 3a1b47391..c8d347b57 100644 --- a/src/tr2/game/demo.c +++ b/src/tr2/game/demo.c @@ -1,6 +1,5 @@ #include "game/demo.h" -#include "decomp/savegame.h" #include "game/camera.h" #include "game/game.h" #include "game/game_flow.h" @@ -14,6 +13,7 @@ #include "game/overlay.h" #include "game/random.h" #include "game/room.h" +#include "game/savegame.h" #include "game/sound.h" #include "game/stats.h" #include "game/text.h" @@ -30,7 +30,7 @@ typedef struct { TEXTSTRING *text; struct { - bool bonus_flag; + GAME_BONUS_FLAG bonus_flag; } old_config; } M_PRIV; @@ -44,13 +44,13 @@ static void M_RestoreConfig(M_PRIV *p); static void M_PrepareConfig(M_PRIV *const p) { - p->old_config.bonus_flag = g_SaveGame.bonus_flag; - g_SaveGame.bonus_flag = false; + p->old_config.bonus_flag = Game_GetBonusFlag(); + Game_SetBonusFlag(GBF_NONE); } static void M_RestoreConfig(M_PRIV *const p) { - g_SaveGame.bonus_flag = p->old_config.bonus_flag; + Game_SetBonusFlag(p->old_config.bonus_flag); } bool Demo_GetInput(void) diff --git a/src/tr2/game/effects.c b/src/tr2/game/effects.c index b5a275e6d..32d72428b 100644 --- a/src/tr2/game/effects.c +++ b/src/tr2/game/effects.c @@ -112,7 +112,7 @@ int16_t Effect_Create(const int16_t room_num) effect->next_active = m_NextEffectActive; m_NextEffectActive = effect_num; - effect->shade = 0x1000; + effect->shade = SHADE_NEUTRAL; return effect_num; } diff --git a/src/tr2/game/fmv.c b/src/tr2/game/fmv.c index 9906fd79d..41001bce8 100644 --- a/src/tr2/game/fmv.c +++ b/src/tr2/game/fmv.c @@ -123,9 +123,8 @@ static bool M_Play(const char *const file_name) ? 0 : g_Config.audio.sound_volume / (float)Sound_GetMaxVolume()); - Video_SetSurfaceSize( - video, Shell_GetCurrentDisplayWidth(), - Shell_GetCurrentDisplayHeight()); + const SHELL_SIZE display_size = Shell_GetCurrentDisplaySize(); + Video_SetSurfaceSize(video, display_size.w, display_size.h); if (g_Config.rendering.render_mode == RM_SOFTWARE) { Video_SetSurfacePixelFormat(video, AV_PIX_FMT_RGB8); GFX_2D_Renderer_SetPalette(renderer_2d, palette); diff --git a/src/tr2/game/game.c b/src/tr2/game/game.c index 478a4fdd8..874a7c157 100644 --- a/src/tr2/game/game.c +++ b/src/tr2/game/game.c @@ -1,12 +1,10 @@ #include "game/game.h" #include "decomp/decomp.h" -#include "decomp/savegame.h" #include "game/camera.h" #include "game/demo.h" #include "game/effects.h" #include "game/game_flow.h" -#include "game/gym.h" #include "game/inventory.h" #include "game/item_actions.h" #include "game/lara/cheat_keys.h" @@ -17,12 +15,14 @@ #include "game/output.h" #include "game/overlay.h" #include "game/room_draw.h" +#include "game/savegame.h" #include "game/shell.h" #include "game/sound.h" #include "game/stats.h" #include "global/vars.h" #include +#include #include bool Game_Start(const GF_LEVEL *const level, const GF_SEQUENCE_CONTEXT seq_ctx) diff --git a/src/tr2/game/game_flow/inventory.c b/src/tr2/game/game_flow/inventory.c index 41ee8c467..b9af92e91 100644 --- a/src/tr2/game/game_flow/inventory.c +++ b/src/tr2/game/game_flow/inventory.c @@ -1,9 +1,9 @@ #include "game/game_flow/inventory.h" -#include "decomp/savegame.h" #include "game/gun/gun.h" #include "game/inventory.h" #include "game/overlay.h" +#include "game/savegame.h" #include "global/vars.h" static int8_t m_SecretInvItems[O_NUMBER_OF] = {}; @@ -14,11 +14,11 @@ static bool m_RemoveFlares = false; static bool m_RemoveMedipacks = false; static void M_ModifyInventory_GunOrAmmo( - START_INFO *start, GF_INV_TYPE type, LARA_GUN_TYPE gun_type); + RESUME_INFO *resume, GF_INV_TYPE type, LARA_GUN_TYPE gun_type); static void M_ModifyInventory_Item(GF_INV_TYPE type, GAME_OBJECT_ID obj_id); static void M_ModifyInventory_GunOrAmmo( - START_INFO *const start, const GF_INV_TYPE type, + RESUME_INFO *const resume, const GF_INV_TYPE type, const LARA_GUN_TYPE gun_type) { const GAME_OBJECT_ID gun_item = Gun_GetGunObject(gun_type); @@ -42,13 +42,13 @@ static void M_ModifyInventory_GunOrAmmo( // clang-format off // TODO: consider moving this to Inv_AddItem switch (gun_type) { - case LGT_PISTOLS: start->has_pistols = 1; break; - case LGT_MAGNUMS: start->has_magnums = 1; break; - case LGT_UZIS: start->has_uzis = 1; break; - case LGT_SHOTGUN: start->has_shotgun = 1; break; - case LGT_HARPOON: start->has_harpoon = 1; break; - case LGT_M16: start->has_m16 = 1; break; - case LGT_GRENADE: start->has_grenade = 1; break; + case LGT_PISTOLS: resume->flags.has_pistols = 1; break; + case LGT_MAGNUMS: resume->flags.has_magnums = 1; break; + case LGT_UZIS: resume->flags.has_uzis = 1; break; + case LGT_SHOTGUN: resume->flags.has_shotgun = 1; break; + case LGT_HARPOON: resume->flags.has_harpoon = 1; break; + case LGT_M16: resume->flags.has_m16 = 1; break; + case LGT_GRENADE: resume->flags.has_grenade = 1; break; default: break; } // clang-format on @@ -137,55 +137,55 @@ void GF_InventoryModifier_Scan(const GF_LEVEL *const level) void GF_InventoryModifier_Apply( const GF_LEVEL *const level, const GF_INV_TYPE type) { - START_INFO *const start = Savegame_GetCurrentInfo(level); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (m_RemoveWeapons) { - start->has_pistols = 0; - start->has_magnums = 0; - start->has_uzis = 0; - start->has_shotgun = 0; - start->has_m16 = 0; - start->has_grenade = 0; - start->has_harpoon = 0; - start->gun_type = LGT_UNARMED; - start->gun_status = LGS_ARMLESS; + resume->flags.has_pistols = 0; + resume->flags.has_magnums = 0; + resume->flags.has_uzis = 0; + resume->flags.has_shotgun = 0; + resume->flags.has_m16 = 0; + resume->flags.has_grenade = 0; + resume->flags.has_harpoon = 0; + resume->equipped_gun_type = LGT_UNARMED; + resume->gun_status = LGS_ARMLESS; } - if (!start->has_pistols && m_Add2InvItems[O_PISTOL_ITEM]) { - start->has_pistols = 1; + if (!resume->flags.has_pistols && m_Add2InvItems[O_PISTOL_ITEM]) { + resume->flags.has_pistols = 1; Inv_AddItem(O_PISTOL_ITEM); - if (start->gun_type == LGT_UNARMED) { - start->gun_type = LGT_PISTOLS; + if (resume->equipped_gun_type == LGT_UNARMED) { + resume->equipped_gun_type = LGT_PISTOLS; } } if (m_RemoveAmmo) { - start->m16_ammo = 0; - start->grenade_ammo = 0; - start->harpoon_ammo = 0; - start->shotgun_ammo = 0; - start->uzi_ammo = 0; - start->magnum_ammo = 0; - start->pistol_ammo = 0; + resume->m16_ammo = 0; + resume->grenade_ammo = 0; + resume->harpoon_ammo = 0; + resume->shotgun_ammo = 0; + resume->uzi_ammo = 0; + resume->magnum_ammo = 0; + resume->pistol_ammo = 0; } if (m_RemoveFlares) { - start->flares = 0; + resume->flares = 0; m_RemoveFlares = false; } if (m_RemoveMedipacks) { - start->large_medipacks = 0; - start->small_medipacks = 0; + resume->large_medipacks = 0; + resume->small_medipacks = 0; m_RemoveMedipacks = false; } - M_ModifyInventory_GunOrAmmo(start, type, LGT_MAGNUMS); - M_ModifyInventory_GunOrAmmo(start, type, LGT_UZIS); - M_ModifyInventory_GunOrAmmo(start, type, LGT_SHOTGUN); - M_ModifyInventory_GunOrAmmo(start, type, LGT_HARPOON); - M_ModifyInventory_GunOrAmmo(start, type, LGT_M16); - M_ModifyInventory_GunOrAmmo(start, type, LGT_GRENADE); + M_ModifyInventory_GunOrAmmo(resume, type, LGT_MAGNUMS); + M_ModifyInventory_GunOrAmmo(resume, type, LGT_UZIS); + M_ModifyInventory_GunOrAmmo(resume, type, LGT_SHOTGUN); + M_ModifyInventory_GunOrAmmo(resume, type, LGT_HARPOON); + M_ModifyInventory_GunOrAmmo(resume, type, LGT_M16); + M_ModifyInventory_GunOrAmmo(resume, type, LGT_GRENADE); M_ModifyInventory_Item(type, O_FLARE_ITEM); M_ModifyInventory_Item(type, O_SMALL_MEDIPACK_ITEM); diff --git a/src/tr2/game/game_flow/sequencer_events.c b/src/tr2/game/game_flow/sequencer_events.c index 4d37f1f14..a1a02505a 100644 --- a/src/tr2/game/game_flow/sequencer_events.c +++ b/src/tr2/game/game_flow/sequencer_events.c @@ -1,5 +1,4 @@ #include "decomp/decomp.h" -#include "decomp/savegame.h" #include "game/camera.h" #include "game/fmv.h" #include "game/game.h" @@ -9,6 +8,7 @@ #include "game/music.h" #include "game/output.h" #include "game/phase.h" +#include "game/savegame.h" #include "game/stats.h" #include "global/vars.h" @@ -56,6 +56,9 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) case GFSC_SAVED: GF_InventoryModifier_Scan(level); + // reset current info to the defaults so that we do not do + // Item_GlobalReplace in the inventory initialization routines too early + Savegame_InitCurrentInfo(); break; case GFSC_SELECT: { @@ -75,7 +78,6 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) } tmp_level = next_level; } - Stats_Reset(); break; } @@ -84,7 +86,6 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) if (level->type == GFL_NORMAL || level->type == GFL_BONUS) { GF_InventoryModifier_Scan(level); GF_InventoryModifier_Apply(level, GF_INV_REGULAR); - Stats_Reset(); } break; } @@ -114,11 +115,16 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) switch (seq_ctx) { case GFSC_SAVED: - ExtractSaveGameInfo(); + const int16_t slot_num = Savegame_GetBoundSlot(); + if (!Savegame_Load(slot_num)) { + LOG_ERROR("Failed to load save file!"); + return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; + } break; default: if (level->type == GFL_NORMAL || level->type == GFL_BONUS) { + Savegame_SetInitialVersion(SAVEGAME_CURRENT_VERSION); GF_InventoryModifier_Scan(Game_GetCurrentLevel()); GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_REGULAR); } @@ -126,11 +132,10 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) } Stats_CalculateStats(); - START_INFO *const resume = Savegame_GetCurrentInfo(level); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); if (resume != nullptr) { const int32_t secret_count = Stats_GetSecrets(); resume->stats.max_secret_count = secret_count; - g_SaveGame.current_stats.max_secret_count = secret_count; } ASSERT(GF_GetCurrentLevel() == level); @@ -142,12 +147,6 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel) gf_cmd = GF_RunGame(level, seq_ctx); } if (gf_cmd.action == GF_LEVEL_COMPLETE) { - // TODO: refactor, currently required to guarantee final statistics are - // accurate prior to jumping to a bonus level. - if (level->type == GFL_NORMAL || level->type == GFL_BONUS) { - START_INFO *const start = Savegame_GetCurrentInfo(level); - start->stats = g_SaveGame.current_stats; - } gf_cmd.action = GF_NOOP; } return gf_cmd; @@ -168,26 +167,21 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleLevelComplete) const GF_LEVEL *const next_level = GF_GetLevelAfter(current_level); if (current_level == GF_GetLastLevel()) { - g_SaveGame.bonus_flag = true; - // TODO: refactor me - START_INFO *const start = Savegame_GetCurrentInfo(current_level); - start->stats = g_SaveGame.current_stats; + g_Config.profile.new_game_plus_unlock = true; + Config_Write(); } - START_INFO *const start = Savegame_GetCurrentInfo(current_level); - start->stats = g_SaveGame.current_stats; - start->available = 0; - g_Config.profile.bonus_level_unlock = - Stats_CheckAllSecretsCollected(GFL_NORMAL); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(current_level); + resume->flags.available = 0; + const bool bonus_level_unlock = Stats_CheckAllSecretsCollected(GFL_NORMAL); if (next_level != nullptr) { Savegame_PersistGameToCurrentInfo(next_level); - g_SaveGame.current_level = next_level->num; } if (next_level == nullptr) { return (GF_COMMAND) { .action = GF_NOOP }; } - if (next_level->type == GFL_BONUS && !g_Config.profile.bonus_level_unlock) { + if (next_level->type == GFL_BONUS && !bonus_level_unlock) { return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } return (GF_COMMAND) { @@ -277,9 +271,10 @@ void GF_PreSequenceHook( g_GF_LaraStartAnim = 0; g_GF_RemoveAmmo = false; g_GF_RemoveWeapons = false; - // TODO: reset bonus flag if seq_ctx == GFSC_SAVED once S_LoadGame logic is - // merged with overall save loading logic. Camera_GetCineData()->position.target_angle = DEG_90; + if (seq_ctx == GFSC_SAVED) { + Game_SetBonusFlag(GBF_NONE); + } } GF_SEQUENCE_CONTEXT GF_SwitchSequenceContext( @@ -298,12 +293,6 @@ GF_SEQUENCE_CONTEXT GF_SwitchSequenceContext( } } -bool GF_ShouldSkipSequenceEvent( - const GF_LEVEL *const level, const GF_SEQUENCE_EVENT *const event) -{ - return false; -} - GF_EVENT_QUEUE_TYPE GF_ShouldDeferSequenceEvent( const GF_SEQUENCE_EVENT_TYPE event_type) { diff --git a/src/tr2/game/game_flow/sequencer_misc.c b/src/tr2/game/game_flow/sequencer_misc.c index 43812ff69..13726a531 100644 --- a/src/tr2/game/game_flow/sequencer_misc.c +++ b/src/tr2/game/game_flow/sequencer_misc.c @@ -1,9 +1,9 @@ -#include "decomp/savegame.h" #include "game/game.h" #include "game/game_flow/common.h" #include "game/game_flow/sequencer.h" #include "game/game_flow/vars.h" #include "game/level.h" +#include "game/savegame.h" #include #include @@ -50,3 +50,8 @@ GF_COMMAND GF_DoLevelSequence( current_level++; } } + +bool GF_HasAvailableStory(const int32_t slot_num) +{ + return false; +} diff --git a/src/tr2/game/game_string.def b/src/tr2/game/game_string.def index 72d31ae82..ee77fca25 100644 --- a/src/tr2/game/game_string.def +++ b/src/tr2/game/game_string.def @@ -29,7 +29,6 @@ GS_DEFINE(STATS_ASSAULT_NO_TIMES_SET, "No Times Set") GS_DEFINE(STATS_ASSAULT_FINISH, "Finish") GS_DEFINE(PASSPORT_EXIT_DEMO, "Exit Demo") GS_DEFINE(MISC_NONE, "None") -GS_DEFINE(MISC_EMPTY_SLOT, "- EMPTY SLOT -") GS_DEFINE(OSD_BILINEAR_FILTER_ON, "Bilinear filter: on") GS_DEFINE(OSD_BILINEAR_FILTER_OFF, "Bilinear filter: off") GS_DEFINE(OSD_WIREFRAME_MODE_ON, "Wireframe mode: on") @@ -42,3 +41,18 @@ GS_DEFINE(OSD_HARDWARE_RENDERING, "Hardware rendering") GS_DEFINE(OSD_SOFTWARE_RENDERING, "Software rendering") GS_DEFINE(OSD_PERSPECTIVE_FILTER_ON, "Perspective correction: on") GS_DEFINE(OSD_PERSPECTIVE_FILTER_OFF, "Perspective correction: off") +GS_DEFINE(DETAIL_FOV, "Field of view") +GS_DEFINE(DETAIL_USE_PSX_FOV, "Use PSX FOV") +GS_DEFINE(DETAIL_DEPTH_BUFFER, "Z-Buffer") +GS_DEFINE(DETAIL_LIGHTING_CONTRAST, "Lighting contrast") +GS_DEFINE(DETAIL_LIGHTING_CONTRAST_LOW, "Low") +GS_DEFINE(DETAIL_LIGHTING_CONTRAST_MEDIUM, "Medium") +GS_DEFINE(DETAIL_LIGHTING_CONTRAST_HIGH, "High") +GS_DEFINE(DETAIL_RENDER_MODE_SOFTWARE, "Software") +GS_DEFINE(DETAIL_RENDER_MODE_HARDWARE, "Hardware") +GS_DEFINE(DETAIL_ASPECT_MODE, "Aspect mode") +GS_DEFINE(DETAIL_ASPECT_MODE_4_3, "4:3") +GS_DEFINE(DETAIL_ASPECT_MODE_16_9, "16:9") +GS_DEFINE(DETAIL_ASPECT_MODE_ANY, "Any") +GS_DEFINE(DETAIL_SCALER, "Scaler") +GS_DEFINE(DETAIL_SIZER, "Sizer") diff --git a/src/tr2/game/gun/gun_misc.c b/src/tr2/game/gun/gun_misc.c index 327d0764b..5561e7e1a 100644 --- a/src/tr2/game/gun/gun_misc.c +++ b/src/tr2/game/gun/gun_misc.c @@ -8,10 +8,12 @@ #include "game/random.h" #include "game/room.h" #include "game/spawn.h" +#include "game/stats.h" #include "global/vars.h" #include #include +#include #include #include #include @@ -114,7 +116,7 @@ void Gun_GetNewTarget(const WEAPON_INFO *const winfo) ITEM *best_target = nullptr; const int16_t max_dist = winfo->target_dist; - for (int32_t i = 0; i < NUM_SLOTS; i++) { + for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { const int16_t item_num = g_BaddieSlots[i].item_num; if (item_num == NO_ITEM || item_num == g_Lara.item_num) { continue; @@ -205,7 +207,7 @@ int32_t Gun_FireWeapon( AMMO_INFO *const ammo = Gun_GetAmmoInfo(weapon_type); ASSERT(ammo != nullptr); - if (ammo == &g_Lara.pistol_ammo || g_SaveGame.bonus_flag) { + if (ammo == &g_Lara.pistol_ammo || Game_IsBonusFlagSet(GBF_NGPLUS)) { ammo->ammo = 1000; } @@ -249,7 +251,7 @@ int32_t Gun_FireWeapon( } } - g_SaveGame.current_stats.ammo_used++; + Stats_AddAmmoUsed(); GAME_VECTOR start; start.pos.x = view_pos.x; @@ -277,7 +279,7 @@ int32_t Gun_FireWeapon( return -1; } } else { - g_SaveGame.current_stats.ammo_hits++; + Stats_AddAmmoHits(); GAME_VECTOR hit_pos; hit_pos.pos.x = view_pos.x + ((best_dist * g_MatrixPtr->_20) >> W2V_SHIFT); @@ -291,7 +293,9 @@ int32_t Gun_FireWeapon( if (item_to_smash != NO_ITEM) { Gun_SmashItem(item_to_smash, weapon_type); } - Gun_HitTarget(target, &hit_pos, winfo->damage); + Gun_HitTarget( + target, &hit_pos, + winfo->damage * (Game_IsBonusFlagSet(GBF_JAPANESE) ? 2 : 1)); return 1; } } @@ -315,7 +319,7 @@ void Gun_HitTarget( { if (item->hit_points > 0 && item->hit_points <= damage && item->object_id != O_DRAGON_FRONT) { - g_SaveGame.current_stats.kills++; + Stats_AddKill(); } Item_TakeDamage(item, damage, true); @@ -325,13 +329,13 @@ void Gun_HitTarget( item->rot.y, item->room_num); } - if (!g_IsMonkAngry + if (!Creature_AreAlliesHostile() && (item->object_id == O_MONK_1 || item->object_id == O_MONK_2)) { CREATURE *const creature = item->data; creature->flags += damage; if ((creature->flags & 0xFFF) > MONK_FRIENDLY_FIRE_THRESHOLD || creature->mood == MOOD_BORED) { - g_IsMonkAngry = true; + Creature_SetAlliesHostile(true); } } } @@ -365,7 +369,7 @@ void Gun_DrawFlash(const LARA_GUN_TYPE weapon_type, const int32_t clip) switch (weapon_type) { case LGT_MAGNUMS: - shade = HIGH_LIGHT; + shade = SHADE_NEUTRAL; y = 215; z = 65; break; @@ -396,7 +400,7 @@ void Gun_DrawFlash(const LARA_GUN_TYPE weapon_type, const int32_t clip) return; default: - shade = LOW_LIGHT; + shade = SHADE_LOW; y = 185; z = 40; break; diff --git a/src/tr2/game/gun/gun_rifle.c b/src/tr2/game/gun/gun_rifle.c index 4f2faa93c..f551e95fc 100644 --- a/src/tr2/game/gun/gun_rifle.c +++ b/src/tr2/game/gun/gun_rifle.c @@ -8,9 +8,11 @@ #include "game/random.h" #include "game/room.h" #include "game/sound.h" +#include "game/stats.h" #include "global/vars.h" #include +#include #include #include #include @@ -195,11 +197,11 @@ void Gun_Rifle_FireHarpoon(void) item->status = IS_ACTIVE; g_Lara.harpoon_ammo.ammo--; - if (g_SaveGame.bonus_flag + if (Game_IsBonusFlagSet(GBF_NGPLUS) && (g_Lara.harpoon_ammo.ammo % HARPOON_RECOIL) == 0) { g_Lara.harpoon_ammo.ammo += HARPOON_RECOIL; } - g_SaveGame.current_stats.ammo_used++; + Stats_AddAmmoUsed(); } void Gun_Rifle_FireGrenade(void) @@ -236,10 +238,10 @@ void Gun_Rifle_FireGrenade(void) Item_AddActive(item_num); item->status = IS_ACTIVE; - if (!g_SaveGame.bonus_flag) { + if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { g_Lara.grenade_ammo.ammo--; } - g_SaveGame.current_stats.ammo_used++; + Stats_AddAmmoUsed(); } void Gun_Rifle_Draw(const LARA_GUN_TYPE weapon_type) diff --git a/src/tr2/game/gym.c b/src/tr2/game/gym.c index 3dbd3bf2c..91ea861b8 100644 --- a/src/tr2/game/gym.c +++ b/src/tr2/game/gym.c @@ -1,125 +1,8 @@ -#include "game/gym.h" - #include "game/game_flow.h" -#include "game/music.h" -#include "game/stats.h" -#include "global/vars.h" -static bool m_IsInventoryOpenEnabled = true; -static bool m_IsAssaultTimerDisplay = false; -static bool m_IsAssaultTimerActive = false; -static uint32_t m_AssaultBestTime = -1; -static ASSAULT_STATS m_AssaultStats = {}; - -static bool M_StoreAssaultTime(uint32_t time); - -static bool M_StoreAssaultTime(const uint32_t time) -{ - int32_t insert_idx = -1; - for (int32_t i = 0; i < MAX_ASSAULT_TIMES; i++) { - if (m_AssaultStats.best_time[i] == 0 - || time < m_AssaultStats.best_time[i]) { - insert_idx = i; - break; - } - } - if (insert_idx == -1) { - return false; - } - - for (int32_t i = MAX_ASSAULT_TIMES - 1; i > insert_idx; i--) { - m_AssaultStats.best_finish[i] = m_AssaultStats.best_finish[i - 1]; - m_AssaultStats.best_time[i] = m_AssaultStats.best_time[i - 1]; - } - - m_AssaultStats.finish_count++; - m_AssaultStats.best_time[insert_idx] = time; - m_AssaultStats.best_finish[insert_idx] = m_AssaultStats.finish_count; - return true; -} +#include bool Gym_IsAccessible(void) { return g_GameFlow.gym_enabled && GF_GetGymLevel() != nullptr; } - -void Gym_SetInventoryOpenEnabled(const bool enabled) -{ - m_IsInventoryOpenEnabled = enabled; -} - -bool Gym_IsInventoryOpenEnabled(void) -{ - return m_IsInventoryOpenEnabled; -} - -bool Gym_IsAssaultTimerDisplay(void) -{ - return m_IsAssaultTimerDisplay; -} - -bool Gym_IsAssaultTimerActive(void) -{ - return m_IsAssaultTimerActive; -} - -ASSAULT_STATS Gym_GetAssaultStats(void) -{ - return m_AssaultStats; -} - -void Gym_ResetAssault(void) -{ - m_IsAssaultTimerActive = false; - m_IsAssaultTimerDisplay = false; -} - -void Gym_StartAssault(void) -{ - g_SaveGame.current_stats.timer = 0; - m_IsAssaultTimerActive = true; - m_IsAssaultTimerDisplay = true; - Stats_StartTimer(); -} - -void Gym_StopAssault(void) -{ - m_IsAssaultTimerActive = false; - m_IsAssaultTimerDisplay = true; -} - -void Gym_FinishAssault(void) -{ - if (!m_IsAssaultTimerActive) { - return; - } - - M_StoreAssaultTime(g_SaveGame.current_stats.timer); - - if ((int32_t)m_AssaultBestTime < 0) { - if (g_SaveGame.current_stats.timer < 100 * FRAMES_PER_SECOND) { - // "Gosh! That was my best time yet!" - Music_Play(MX_GYM_HINT_15, MPM_ALWAYS); - m_AssaultBestTime = g_SaveGame.current_stats.timer; - } else { - // "Congratulations! You did it! But perhaps I could've been - // faster." - Music_Play(MX_GYM_HINT_17, MPM_ALWAYS); - m_AssaultBestTime = 100 * FRAMES_PER_SECOND; - } - } else if (g_SaveGame.current_stats.timer < m_AssaultBestTime) { - // "Gosh! That was my best time yet!" - Music_Play(MX_GYM_HINT_15, MPM_ALWAYS); - m_AssaultBestTime = g_SaveGame.current_stats.timer; - } else if ( - g_SaveGame.current_stats.timer - < m_AssaultBestTime + 5 * FRAMES_PER_SECOND) { - // "Almost. Perhaps another try and I might beat it." - Music_Play(MX_GYM_HINT_16, MPM_ALWAYS); - } else { - // "Great. But nowhere near my best time." - Music_Play(MX_GYM_HINT_14, MPM_ALWAYS); - } - - m_IsAssaultTimerActive = false; -} diff --git a/src/tr2/game/input.h b/src/tr2/game/input.h index e4e59f254..6f2d02c9b 100644 --- a/src/tr2/game/input.h +++ b/src/tr2/game/input.h @@ -1,5 +1,3 @@ #pragma once #include - -const char *Input_GetRoleName(INPUT_ROLE role); diff --git a/src/tr2/game/inventory.c b/src/tr2/game/inventory.c index 1f1fd7197..26e705e50 100644 --- a/src/tr2/game/inventory.c +++ b/src/tr2/game/inventory.c @@ -6,8 +6,18 @@ #include "game/stats.h" #include "global/vars.h" +#include +#include #include +static int32_t M_GetFlareQuantity(void); + +static int32_t M_GetFlareQuantity(void) +{ + return Game_IsBonusFlagSet(GBF_JAPANESE) ? FLARE_AMMO_JAPANESE_QTY + : FLARE_AMMO_QTY; +} + bool Inv_AddItem(const GAME_OBJECT_ID obj_id) { const GAME_OBJECT_ID inv_obj_id = Inv_GetItemOption(obj_id); @@ -22,7 +32,7 @@ bool Inv_AddItem(const GAME_OBJECT_ID obj_id) for (int32_t i = 0; i < source->count; i++) { if (source->items[i]->object_id == inv_obj_id) { const int32_t qty = - obj_id == O_FLARES_ITEM ? FLARE_AMMO_QTY : 1; + obj_id == O_FLARES_ITEM ? M_GetFlareQuantity() : 1; source->qtys[i] += qty; CLAMPG(source->qtys[i], MAX_QTY); return true; @@ -183,7 +193,7 @@ bool Inv_AddItem(const GAME_OBJECT_ID obj_id) case O_FLARES_ITEM: case O_FLARES_OPTION: - for (int32_t i = 0; i < FLARE_AMMO_QTY; i++) { + for (int32_t i = 0; i < M_GetFlareQuantity(); i++) { Inv_AddItem(O_FLARE_ITEM); } return true; diff --git a/src/tr2/game/inventory_ring/control.c b/src/tr2/game/inventory_ring/control.c index 6aea9a1cd..900198691 100644 --- a/src/tr2/game/inventory_ring/control.c +++ b/src/tr2/game/inventory_ring/control.c @@ -1,11 +1,9 @@ #include "game/inventory_ring/control.h" -#include "decomp/savegame.h" #include "game/clock.h" #include "game/demo.h" #include "game/game.h" #include "game/game_flow.h" -#include "game/gym.h" #include "game/input.h" #include "game/inventory.h" #include "game/inventory_ring/draw.h" @@ -15,6 +13,7 @@ #include "game/option/option.h" #include "game/output.h" #include "game/overlay.h" +#include "game/savegame.h" #include "game/shell.h" #include "game/sound.h" #include "game/stats.h" @@ -22,6 +21,8 @@ #include "global/vars.h" #include +#include +#include #include #include #include @@ -31,8 +32,8 @@ #include -#define TITLE_RING_OBJECTS 3 -#define OPTION_RING_OBJECTS 3 +#define TITLE_RING_OBJECTS 4 +#define OPTION_RING_OBJECTS 4 #define INV_RING_FADE_TIME_FAST \ (INV_RING_CLOSE_FRAMES / INV_RING_FRAMES / (double)LOGIC_FPS) #define INV_RING_FADE_TIME_TITLE_FINISH 0.25 @@ -53,7 +54,7 @@ static GF_COMMAND M_Control(INV_RING *ring); static void M_ShowAmmoQuantity(const char *const fmt, const int32_t qty) { - if (!g_SaveGame.bonus_flag) { + if (!Game_IsBonusFlagSet(GBF_NGPLUS)) { InvRing_ShowItemQuantity(fmt, qty); } } @@ -192,11 +193,6 @@ static GF_COMMAND M_Finish(INV_RING *const ring, const bool apply_changes) // first passport page: load game. if (apply_changes) { Inv_RemoveAllItems(); - if (!S_LoadGame(g_Inv_ExtraData[1])) { - return (GF_COMMAND) { - .action = GF_EXIT_TO_TITLE, - }; - } } return (GF_COMMAND) { .action = GF_START_SAVED_GAME, @@ -243,8 +239,7 @@ static GF_COMMAND M_Finish(INV_RING *const ring, const bool apply_changes) } else { if (apply_changes) { Music_Unpause(); - CreateSaveGameInfo(); - S_SaveGame(g_Inv_ExtraData[1]); + Savegame_Save(g_Inv_ExtraData[1]); } return (GF_COMMAND) { .action = GF_NOOP }; } diff --git a/src/tr2/game/inventory_ring/draw.c b/src/tr2/game/inventory_ring/draw.c index 609be4fee..a7f7141a6 100644 --- a/src/tr2/game/inventory_ring/draw.c +++ b/src/tr2/game/inventory_ring/draw.c @@ -1,12 +1,13 @@ #include "game/inventory_ring/draw.h" -#include "decomp/savegame.h" #include "game/console/common.h" +#include "game/game.h" #include "game/input.h" #include "game/inventory_ring/control.h" #include "game/option/option.h" #include "game/output.h" #include "game/overlay.h" +#include "game/savegame.h" #include "global/vars.h" #include @@ -67,9 +68,9 @@ static void M_DrawItem( { if (ring->motion.status != RNG_FADING_OUT && ring->motion.status != RNG_DONE && inv_item == ring->list[ring->current_object] && !ring->rotating) { - Output_SetLightAdder(HIGH_LIGHT); + Output_SetLightAdder(SHADE_NEUTRAL); } else { - Output_SetLightAdder(LOW_LIGHT); + Output_SetLightAdder(SHADE_LOW); } Matrix_TranslateRel(0, inv_item->y_trans, inv_item->z_trans); @@ -90,16 +91,18 @@ static void M_DrawItem( ANIM_FRAME *frame2; const int32_t frac = M_GetFrames(ring, inv_item, &frame1, &frame2, &rate); if (inv_item->object_id == O_COMPASS_OPTION) { + const RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); const int32_t total_seconds = - g_SaveGame.current_stats.timer / FRAMES_PER_SECOND; + current_info->stats.timer / FRAMES_PER_SECOND; const int32_t hours = (total_seconds % 43200) * DEG_1 * -360 / 43200; const int32_t minutes = (total_seconds % 3600) * DEG_1 * -360 / 3600; const int32_t seconds = (total_seconds % 60) * DEG_1 * -360 / 60; const int16_t extra_rotation[3] = { hours, minutes, seconds }; - Object_GetBone(obj, 3)->rot_z = true; - Object_GetBone(obj, 4)->rot_z = true; - Object_GetBone(obj, 5)->rot_z = true; + Object_GetBone(obj, 3)->rot.z = true; + Object_GetBone(obj, 4)->rot.z = true; + Object_GetBone(obj, 5)->rot.z = true; Object_DrawInterpolatedObject( obj, inv_item->meshes_drawn, extra_rotation, frame1, frame2, frac, rate); diff --git a/src/tr2/game/inventory_ring/vars.c b/src/tr2/game/inventory_ring/vars.c index f5143c0e2..4411b0314 100644 --- a/src/tr2/game/inventory_ring/vars.c +++ b/src/tr2/game/inventory_ring/vars.c @@ -2,7 +2,6 @@ #include "global/vars.h" -INVENTORY_MODE g_Inv_Mode = INV_TITLE_MODE; int16_t g_Inv_Chosen = -1; int32_t g_Inv_ExtraData[8]; int32_t g_Inv_NFrames = 2; @@ -44,11 +43,12 @@ INV_RING_SOURCE g_InvRing_Source[RT_NUMBER_OF] = { }, }, [RT_OPTION] = { - .count = 4, + .count = 5, .current = 0, - .qtys = { 1, 1, 1, 1 }, + .qtys = { 1, 1, 1, 1, 1 }, .items = { &g_InvRing_Item_Passport, + &g_InvRing_Item_Graphics, &g_InvRing_Item_Controls, &g_InvRing_Item_Sound, &g_InvRing_Item_Photo, diff --git a/src/tr2/game/inventory_ring/vars.h b/src/tr2/game/inventory_ring/vars.h index bedf75446..a180596e7 100644 --- a/src/tr2/game/inventory_ring/vars.h +++ b/src/tr2/game/inventory_ring/vars.h @@ -4,7 +4,6 @@ #include -extern INVENTORY_MODE g_Inv_Mode; extern int16_t g_Inv_Chosen; extern int32_t g_Inv_ExtraData[8]; diff --git a/src/tr2/game/item_actions.c b/src/tr2/game/item_actions.c index eec829171..fd57134ca 100644 --- a/src/tr2/game/item_actions.c +++ b/src/tr2/game/item_actions.c @@ -1,7 +1,6 @@ #include "game/item_actions.h" #include "game/camera.h" -#include "game/gym.h" #include "game/lara/hair.h" #include "game/random.h" #include "game/room.h" @@ -12,6 +11,7 @@ #include "global/vars.h" #include +#include #include #include diff --git a/src/tr2/game/items.c b/src/tr2/game/items.c index aa26ba927..b04adfdce 100644 --- a/src/tr2/game/items.c +++ b/src/tr2/game/items.c @@ -10,12 +10,14 @@ #include #include +#include #include #include #include #include #include +static BOUNDS_16 m_NullBounds = {}; static BOUNDS_16 m_InterpolatedBounds = {}; static OBJECT_BOUNDS M_ConvertBounds(const int16_t *bounds_in); @@ -122,7 +124,8 @@ void Item_Initialise(const int16_t item_num) Room_GetWorldSector(room, item->pos.x, item->pos.z); item->floor = sector->floor.height; - if (g_SaveGame.bonus_flag && GF_GetCurrentLevel()->type != GFL_DEMO) { + if (Game_IsBonusFlagSet(GBF_NGPLUS) + && GF_GetCurrentLevel()->type != GFL_DEMO) { item->hit_points *= 2; } @@ -185,36 +188,6 @@ int16_t Item_GetHeight(const ITEM *const item) return height; } -int32_t Item_TestBoundsCollide( - const ITEM *const src_item, const ITEM *const dst_item, - const int32_t radius) -{ - const BOUNDS_16 *const src_bounds = &Item_GetBestFrame(src_item)->bounds; - const BOUNDS_16 *const dst_bounds = &Item_GetBestFrame(dst_item)->bounds; - - if (src_item->pos.y + src_bounds->max.y - <= dst_item->pos.y + dst_bounds->min.y - || src_item->pos.y + src_bounds->min.y - >= dst_item->pos.y + dst_bounds->max.y) { - return false; - } - - const int32_t c = Math_Cos(src_item->rot.y); - const int32_t s = Math_Sin(src_item->rot.y); - const int32_t dx = dst_item->pos.x - src_item->pos.x; - const int32_t dz = dst_item->pos.z - src_item->pos.z; - const int32_t rx = (c * dx - s * dz) >> W2V_SHIFT; - const int32_t rz = (c * dz + s * dx) >> W2V_SHIFT; - - // clang-format off - return ( - rx >= src_bounds->min.x - radius && - rx <= src_bounds->max.x + radius && - rz >= src_bounds->min.z - radius && - rz <= src_bounds->max.z + radius); - // clang-format on -} - int32_t Item_TestPosition( const int16_t *const bounds_in, const ITEM *const src_item, const ITEM *const dst_item) @@ -304,33 +277,14 @@ void Item_AlignPosition( dst_item->pos.z = new_pos.z; } -int32_t Item_IsTriggerActive(ITEM *const item) -{ - const bool ok = !(item->flags & IF_REVERSE); - - if ((item->flags & IF_CODE_BITS) != IF_CODE_BITS) { - return !ok; - } - - if (!item->timer) { - return ok; - } - - if (item->timer == -1) { - return !ok; - } - - item->timer--; - if (item->timer == 0) { - item->timer = -1; - } - - return ok; -} - -int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate) +int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frames[], int32_t *rate) { const ANIM *const anim = Item_GetAnim(item); + if (anim->frame_ptr == nullptr) { + frames[0] = nullptr; + return 0; + } + const int32_t cur_frame_num = item->frame_num - anim->frame_base; const int32_t last_frame_num = anim->frame_end - anim->frame_base; const int32_t key_frame_span = anim->interpolation; @@ -349,8 +303,8 @@ int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate) } } - frmptr[0] = &anim->frame_ptr[first_key_frame_num]; - frmptr[1] = &anim->frame_ptr[second_key_frame_num]; + frames[0] = &anim->frame_ptr[first_key_frame_num]; + frames[1] = &anim->frame_ptr[second_key_frame_num]; // OG if (g_Config.rendering.fps == 30) { @@ -381,34 +335,38 @@ int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate) return final * 10; } -BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *const item) +const BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *const item) { int32_t rate; - ANIM_FRAME *frmptr[2]; - const int32_t frac = Item_GetFrames(item, frmptr, &rate); - if (!frac) { - return &frmptr[0]->bounds; + ANIM_FRAME *frames[2]; + const int32_t frac = Item_GetFrames(item, frames, &rate); + if (frames[0] == nullptr) { + return &m_NullBounds; + } + + if (frac == 0) { + return &frames[0]->bounds; } #define CALC(target, b1, b2, prop) \ target->prop = (b1)->prop + ((((b2)->prop - (b1)->prop) * frac) / rate); BOUNDS_16 *const result = &m_InterpolatedBounds; - CALC(result, &frmptr[0]->bounds, &frmptr[1]->bounds, min.x); - CALC(result, &frmptr[0]->bounds, &frmptr[1]->bounds, max.x); - CALC(result, &frmptr[0]->bounds, &frmptr[1]->bounds, min.y); - CALC(result, &frmptr[0]->bounds, &frmptr[1]->bounds, max.y); - CALC(result, &frmptr[0]->bounds, &frmptr[1]->bounds, min.z); - CALC(result, &frmptr[0]->bounds, &frmptr[1]->bounds, max.z); + CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.x); + CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.x); + CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.y); + CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.y); + CALC(result, &frames[0]->bounds, &frames[1]->bounds, min.z); + CALC(result, &frames[0]->bounds, &frames[1]->bounds, max.z); return result; } ANIM_FRAME *Item_GetBestFrame(const ITEM *const item) { - ANIM_FRAME *frmptr[2]; + ANIM_FRAME *frames[2]; int32_t rate; - const int32_t frac = Item_GetFrames(item, frmptr, &rate); - return frmptr[(frac > rate / 2) ? 1 : 0]; + const int32_t frac = Item_GetFrames(item, frames, &rate); + return frames[(frac > rate / 2) ? 1 : 0]; } bool Item_IsNearItem( @@ -490,18 +448,7 @@ int32_t Item_Explode( Matrix_TranslateRel32(bone->pos); Matrix_Rot16(best_frame->mesh_rots[i]); - - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); bit <<= 1; if ((mesh_bits & bit) && (item->mesh_bits & bit)) { diff --git a/src/tr2/game/items.h b/src/tr2/game/items.h index f60ebe7a2..6a26aa675 100644 --- a/src/tr2/game/items.h +++ b/src/tr2/game/items.h @@ -8,15 +8,11 @@ void Item_Control(void); void Item_ClearKilled(void); void Item_ShiftCol(ITEM *item, COLL_INFO *coll); void Item_UpdateRoom(ITEM *item, int32_t height); -int32_t Item_TestBoundsCollide( - const ITEM *src_item, const ITEM *dst_item, int32_t radius); int32_t Item_TestPosition( const int16_t *bounds, const ITEM *src_item, const ITEM *dst_item); void Item_AlignPosition( const XYZ_32 *vec, const ITEM *src_item, ITEM *dst_item); -int32_t Item_IsTriggerActive(ITEM *item); int32_t Item_GetFrames(const ITEM *item, ANIM_FRAME *frmptr[], int32_t *rate); -BOUNDS_16 *Item_GetBoundsAccurate(const ITEM *item); bool Item_IsNearItem(const ITEM *item, const XYZ_32 *pos, int32_t distance); bool Item_IsSmashable(const ITEM *item); diff --git a/src/tr2/game/lara/cheat.c b/src/tr2/game/lara/cheat.c index 9e9b0672f..1f0a9e8a7 100644 --- a/src/tr2/game/lara/cheat.c +++ b/src/tr2/game/lara/cheat.c @@ -301,13 +301,23 @@ bool Lara_Cheat_GiveAllItems(void) return true; } -bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z) +bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z, int16_t room_num) { - int16_t room_num = Room_GetIndexFromPos(x, y, z); + if (room_num == NO_ROOM) { + room_num = Room_GetIndexFromPos(x, y, z); + } if (room_num == NO_ROOM) { return false; } + const ROOM *const room = Room_Get(room_num); + if (room->flip_status == RFS_FLIPPED && Room_GetFlipStatus()) { + room_num = Room_GetFlippedBaseRoom(room_num); + if (room_num == NO_ROOM) { + return false; + } + } + const SECTOR *sector = Room_GetSector(x, y, z, &room_num); int16_t height = Room_GetHeight(sector, x, y, z); @@ -329,12 +339,9 @@ bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z) .y = y, .z = ROUND_TO_SECTOR(z + dz * unit) + WALL_L / 2, }; - room_num = Room_GetIndexFromPos(point.x, point.y, point.z); - if (room_num == NO_ROOM) { - continue; - } sector = Room_GetSector(point.x, point.y, point.z, &room_num); - height = Room_GetHeight(sector, point.x, point.y, point.z); + height = + Room_GetHeightEx(sector, point.x, point.y, point.z, true); if (height == NO_HEIGHT) { continue; } @@ -361,12 +368,8 @@ bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z) } } - room_num = Room_GetIndexFromPos(x, y, z); - if (room_num == NO_ROOM) { - return false; - } sector = Room_GetSector(x, y, z, &room_num); - height = Room_GetHeight(sector, x, y, z); + height = Room_GetHeightEx(sector, x, y, z, true); if (height == NO_HEIGHT) { return false; } diff --git a/src/tr2/game/lara/cheat.h b/src/tr2/game/lara/cheat.h index a6c051cf7..09ce86840 100644 --- a/src/tr2/game/lara/cheat.h +++ b/src/tr2/game/lara/cheat.h @@ -10,5 +10,4 @@ bool Lara_Cheat_GiveAllKeys(void); bool Lara_Cheat_GiveAllGuns(void); bool Lara_Cheat_GiveAllItems(void); bool Lara_Cheat_OpenNearestDoor(void); -bool Lara_Cheat_Teleport(int32_t x, int32_t y, int32_t z); bool Lara_Cheat_KillEnemy(int16_t item_num); diff --git a/src/tr2/game/lara/control.c b/src/tr2/game/lara/control.c index abfb59195..726c9dcdc 100644 --- a/src/tr2/game/lara/control.c +++ b/src/tr2/game/lara/control.c @@ -1,12 +1,10 @@ #include "game/lara/control.h" -#include "decomp/savegame.h" #include "decomp/skidoo.h" #include "game/camera.h" #include "game/creature.h" #include "game/game.h" #include "game/gun/gun.h" -#include "game/gym.h" #include "game/input.h" #include "game/inventory.h" #include "game/item_actions.h" @@ -18,10 +16,13 @@ #include "game/lara/state.h" #include "game/music.h" #include "game/room.h" +#include "game/savegame.h" #include "game/sound.h" #include "game/spawn.h" +#include "game/stats.h" #include "global/vars.h" +#include #include #include #include @@ -680,11 +681,7 @@ void Lara_Control(const int16_t item_num) break; } - g_SaveGame.current_stats.distance += Math_Sqrt( - SQUARE(item->pos.z - g_Lara.last_pos.z) - + SQUARE(item->pos.y - g_Lara.last_pos.y) - + SQUARE(item->pos.x - g_Lara.last_pos.x)); - + Stats_AddDistanceTravelled(item->pos, g_Lara.last_pos); g_Lara.last_pos = item->pos; } @@ -740,7 +737,7 @@ void Lara_UseItem(const GAME_OBJECT_ID obj_id) CLAMPG(item->hit_points, LARA_MAX_HITPOINTS); Inv_RemoveItem(O_SMALL_MEDIPACK_ITEM); Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS); - g_SaveGame.current_stats.medipacks++; + Stats_AddMedipacksUsed(0.5); } break; @@ -750,7 +747,7 @@ void Lara_UseItem(const GAME_OBJECT_ID obj_id) item->hit_points = LARA_MAX_HITPOINTS; Inv_RemoveItem(O_LARGE_MEDIPACK_ITEM); Sound_Effect(SFX_MENU_MEDI, nullptr, SPM_ALWAYS); - g_SaveGame.current_stats.medipacks += 2; + Stats_AddMedipacksUsed(1); } break; @@ -858,72 +855,72 @@ void Lara_InitialiseInventory(const GF_LEVEL *const level) Inv_RemoveAllItems(); Inv_AddItem(O_COMPASS_ITEM); - START_INFO *const start = Savegame_GetCurrentInfo(level); - if (start != nullptr) { + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + if (resume != nullptr) { g_Lara.pistol_ammo.ammo = 1000; - if (start->has_pistols) { + if (resume->flags.has_pistols) { Inv_AddItem(O_PISTOL_ITEM); } - if (start->has_magnums) { + if (resume->flags.has_magnums) { Inv_AddItem(O_MAGNUM_ITEM); - g_Lara.magnum_ammo.ammo = start->magnum_ammo; + g_Lara.magnum_ammo.ammo = resume->magnum_ammo; Item_GlobalReplace(O_MAGNUM_ITEM, O_MAGNUM_AMMO_ITEM); } else { - Inv_AddItemNTimes(O_MAGNUM_AMMO_ITEM, start->magnum_ammo / 40); + Inv_AddItemNTimes(O_MAGNUM_AMMO_ITEM, resume->magnum_ammo / 40); g_Lara.magnum_ammo.ammo = 0; } - if (start->has_uzis) { + if (resume->flags.has_uzis) { Inv_AddItem(O_UZI_ITEM); - g_Lara.uzi_ammo.ammo = start->uzi_ammo; + g_Lara.uzi_ammo.ammo = resume->uzi_ammo; Item_GlobalReplace(O_UZI_ITEM, O_UZI_AMMO_ITEM); } else { - Inv_AddItemNTimes(O_UZI_AMMO_ITEM, start->uzi_ammo / 80); + Inv_AddItemNTimes(O_UZI_AMMO_ITEM, resume->uzi_ammo / 80); g_Lara.uzi_ammo.ammo = 0; } - if (start->has_shotgun) { + if (resume->flags.has_shotgun) { Inv_AddItem(O_SHOTGUN_ITEM); - g_Lara.shotgun_ammo.ammo = start->shotgun_ammo; + g_Lara.shotgun_ammo.ammo = resume->shotgun_ammo; Item_GlobalReplace(O_SHOTGUN_ITEM, O_SHOTGUN_AMMO_ITEM); } else { - Inv_AddItemNTimes(O_SHOTGUN_AMMO_ITEM, start->shotgun_ammo / 12); + Inv_AddItemNTimes(O_SHOTGUN_AMMO_ITEM, resume->shotgun_ammo / 12); g_Lara.shotgun_ammo.ammo = 0; } - if (start->has_m16) { + if (resume->flags.has_m16) { Inv_AddItem(O_M16_ITEM); - g_Lara.m16_ammo.ammo = start->m16_ammo; + g_Lara.m16_ammo.ammo = resume->m16_ammo; Item_GlobalReplace(O_M16_ITEM, O_M16_AMMO_ITEM); } else { - Inv_AddItemNTimes(O_M16_AMMO_ITEM, start->m16_ammo / 40); + Inv_AddItemNTimes(O_M16_AMMO_ITEM, resume->m16_ammo / 40); g_Lara.m16_ammo.ammo = 0; } - if (start->has_grenade) { + if (resume->flags.has_grenade) { Inv_AddItem(O_GRENADE_ITEM); - g_Lara.grenade_ammo.ammo = start->grenade_ammo; + g_Lara.grenade_ammo.ammo = resume->grenade_ammo; Item_GlobalReplace(O_GRENADE_ITEM, O_GRENADE_AMMO_ITEM); } else { - Inv_AddItemNTimes(O_GRENADE_AMMO_ITEM, start->grenade_ammo / 2); + Inv_AddItemNTimes(O_GRENADE_AMMO_ITEM, resume->grenade_ammo / 2); g_Lara.grenade_ammo.ammo = 0; } - if (start->has_harpoon) { + if (resume->flags.has_harpoon) { Inv_AddItem(O_HARPOON_ITEM); - g_Lara.harpoon_ammo.ammo = start->harpoon_ammo; + g_Lara.harpoon_ammo.ammo = resume->harpoon_ammo; Item_GlobalReplace(O_HARPOON_ITEM, O_HARPOON_AMMO_ITEM); } else { - Inv_AddItemNTimes(O_HARPOON_AMMO_ITEM, start->harpoon_ammo / 3); + Inv_AddItemNTimes(O_HARPOON_AMMO_ITEM, resume->harpoon_ammo / 3); g_Lara.harpoon_ammo.ammo = 0; } - Inv_AddItemNTimes(O_FLARE_ITEM, start->flares); - Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, start->small_medipacks); - Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, start->large_medipacks); + Inv_AddItemNTimes(O_FLARE_ITEM, resume->flares); + Inv_AddItemNTimes(O_SMALL_MEDIPACK_ITEM, resume->small_medipacks); + Inv_AddItemNTimes(O_LARGE_MEDIPACK_ITEM, resume->large_medipacks); - g_Lara.last_gun_type = start->gun_type; + g_Lara.last_gun_type = resume->equipped_gun_type; } g_Lara.gun_status = LGS_ARMLESS; @@ -940,16 +937,16 @@ void Lara_InitialiseMeshes(const GF_LEVEL *const level) Lara_SwapSingleMesh(i, O_LARA); } - const START_INFO *const start = Savegame_GetCurrentInfo(level); - if (start == nullptr) { + const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + if (resume == nullptr) { return; } GAME_OBJECT_ID holster_obj_id = NO_OBJECT; - if (start->gun_type != LGT_UNARMED) { - if (start->gun_type == LGT_MAGNUMS) { + if (resume->equipped_gun_type != LGT_UNARMED) { + if (resume->equipped_gun_type == LGT_MAGNUMS) { holster_obj_id = O_LARA_MAGNUMS; - } else if (start->gun_type == LGT_UZIS) { + } else if (resume->equipped_gun_type == LGT_UZIS) { holster_obj_id = O_LARA_UZIS; } else { holster_obj_id = O_LARA_PISTOLS; @@ -961,11 +958,11 @@ void Lara_InitialiseMeshes(const GF_LEVEL *const level) Lara_SwapSingleMesh(LM_THIGH_R, holster_obj_id); } - if (start->gun_type == LGT_FLARE) { + if (resume->equipped_gun_type == LGT_FLARE) { Lara_SwapSingleMesh(LM_HAND_L, O_LARA_FLARE); } - switch (start->gun_type) { + switch (resume->equipped_gun_type) { case LGT_M16: g_Lara.back_gun = O_LARA_M16; return; @@ -977,15 +974,18 @@ void Lara_InitialiseMeshes(const GF_LEVEL *const level) case LGT_HARPOON: g_Lara.back_gun = O_LARA_HARPOON; return; + + default: + break; } - if (start->has_shotgun) { + if (resume->flags.has_shotgun) { g_Lara.back_gun = O_LARA_SHOTGUN; - } else if (start->has_m16) { + } else if (resume->flags.has_m16) { g_Lara.back_gun = O_LARA_M16; - } else if (start->has_grenade) { + } else if (resume->flags.has_grenade) { g_Lara.back_gun = O_LARA_GRENADE; - } else if (start->has_harpoon) { + } else if (resume->flags.has_harpoon) { g_Lara.back_gun = O_LARA_HARPOON; } } diff --git a/src/tr2/game/lara/misc.c b/src/tr2/game/lara/misc.c index 309e2fa17..c510e6895 100644 --- a/src/tr2/game/lara/misc.c +++ b/src/tr2/game/lara/misc.c @@ -912,7 +912,7 @@ void Lara_GetJointAbsPosition_I( if (g_Lara.gun_type == LGT_FLARE) { Matrix_Interpolate(); - Matrix_TranslateRel32(bone[LM_UARM_R - 1].pos); + Matrix_TranslateRel32(bone[LM_UARM_L - 1].pos); if (g_Lara.flare_control_left) { const LARA_ARM *const arm = &g_Lara.left_arm; const ANIM *const anim = Anim_GetAnim(arm->anim_num); @@ -948,7 +948,6 @@ void Lara_GetJointAbsPosition_I( vec->x = item->pos.x + (g_MatrixPtr->_03 >> W2V_SHIFT); vec->y = item->pos.y + (g_MatrixPtr->_13 >> W2V_SHIFT); vec->z = item->pos.z + (g_MatrixPtr->_23 >> W2V_SHIFT); - Matrix_Pop(); } @@ -1026,11 +1025,12 @@ void Lara_BaddieCollision(ITEM *lara_item, COLL_INFO *coll) } void Lara_Push( - const ITEM *const item, ITEM *const lara_item, COLL_INFO *const coll, - const bool hit_on, const bool big_push) + const ITEM *const item, COLL_INFO *const coll, const bool hit_on, + const bool big_push) { - int32_t dx = lara_item->pos.x - item->pos.x; - int32_t dz = lara_item->pos.z - item->pos.z; + ITEM *const target_item = Lara_GetItem(); + int32_t dx = target_item->pos.x - item->pos.x; + int32_t dz = target_item->pos.z - item->pos.z; const int32_t c = Math_Cos(item->rot.y); const int32_t s = Math_Sin(item->rot.y); int32_t rx = (c * dx - s * dz) >> W2V_SHIFT; @@ -1068,8 +1068,8 @@ void Lara_Push( rz = min_z; } - lara_item->pos.x = item->pos.x + ((rz * s + rx * c) >> W2V_SHIFT); - lara_item->pos.z = item->pos.z + ((rz * c - rx * s) >> W2V_SHIFT); + target_item->pos.x = item->pos.x + ((rz * s + rx * c) >> W2V_SHIFT); + target_item->pos.z = item->pos.z + ((rz * c - rx * s) >> W2V_SHIFT); rz = (bounds->max.z + bounds->min.z) / 2; rx = (bounds->max.x + bounds->min.x) / 2; @@ -1077,7 +1077,7 @@ void Lara_Push( dz -= (c * rz - s * rx) >> W2V_SHIFT; if (hit_on && bounds->max.y - bounds->min.y > STEP_L) { - M_TakeHit(lara_item, dx, dz); + M_TakeHit(target_item, dx, dz); } int16_t old_facing = coll->facing; @@ -1085,20 +1085,20 @@ void Lara_Push( coll->bad_neg = -STEPUP_HEIGHT; coll->bad_ceiling = 0; coll->facing = Math_Atan( - lara_item->pos.z - coll->old.z, lara_item->pos.x - coll->old.x); + target_item->pos.z - coll->old.z, target_item->pos.x - coll->old.x); Collide_GetCollisionInfo( - coll, lara_item->pos.x, lara_item->pos.y, lara_item->pos.z, - lara_item->room_num, LARA_HEIGHT); + coll, target_item->pos.x, target_item->pos.y, target_item->pos.z, + target_item->room_num, LARA_HEIGHT); coll->facing = old_facing; if (coll->coll_type != COLL_NONE) { - lara_item->pos.x = coll->old.x; - lara_item->pos.z = coll->old.z; + target_item->pos.x = coll->old.x; + target_item->pos.z = coll->old.z; } else { - coll->old.x = lara_item->pos.x; - coll->old.y = lara_item->pos.y; - coll->old.z = lara_item->pos.z; - Item_UpdateRoom(lara_item, -WALL_SHIFT); + coll->old.x = target_item->pos.x; + coll->old.y = target_item->pos.y; + coll->old.z = target_item->pos.z; + Item_UpdateRoom(target_item, -10); } } diff --git a/src/tr2/game/lara/misc.h b/src/tr2/game/lara/misc.h index 36a6379a3..39286a548 100644 --- a/src/tr2/game/lara/misc.h +++ b/src/tr2/game/lara/misc.h @@ -46,9 +46,6 @@ void Lara_GetJointAbsPosition_I( void Lara_BaddieCollision(ITEM *lara_item, COLL_INFO *coll); void Lara_TakeHit(ITEM *lara_item, const COLL_INFO *coll); -void Lara_Push( - const ITEM *item, ITEM *lara_item, COLL_INFO *coll, bool hit_on, - bool big_push); int32_t Lara_MovePosition(XYZ_32 *vec, ITEM *item, ITEM *lara_item); int32_t Lara_IsNearItem(const XYZ_32 *pos, int32_t distance); @@ -75,8 +72,6 @@ void Lara_SwimCollision(ITEM *item, COLL_INFO *coll); void Lara_WaterCurrent(COLL_INFO *coll); -void Lara_CatchFire(void); - void Lara_TouchLava(ITEM *item); // Returns true if Lara has the M16 equipped and is in either anim state: 0 diff --git a/src/tr2/game/level.c b/src/tr2/game/level.c index 76eb04716..6ff60b7d0 100644 --- a/src/tr2/game/level.c +++ b/src/tr2/game/level.c @@ -1,12 +1,10 @@ #include "game/level.h" #include "decomp/decomp.h" -#include "decomp/savegame.h" #include "game/camera.h" #include "game/effects.h" #include "game/game.h" #include "game/game_flow.h" -#include "game/gym.h" #include "game/items.h" #include "game/lara/control.h" #include "game/lot.h" @@ -16,6 +14,7 @@ #include "game/random.h" #include "game/render/common.h" #include "game/room.h" +#include "game/savegame.h" #include "game/shell.h" #include "game/sound.h" #include "game/stats.h" @@ -23,11 +22,13 @@ #include "global/vars.h" #include +#include #include #include #include #include #include +#include #include #include #include @@ -217,7 +218,6 @@ static void M_LoadFromFile(const GF_LEVEL *const level) BENCHMARK benchmark = Benchmark_Start(); const char *full_path = File_GetFullPath(level->path); - strcpy(g_LevelFileName, full_path); VFILE *const file = VFile_CreateFromPath(full_path); Memory_FreePointer(&full_path); @@ -277,7 +277,7 @@ static void M_CompleteSetup(const GF_LEVEL *const level) Level_LoadTexturePages(); Level_LoadPalettes(); Level_LoadFaces(); - Output_InitialiseNamedColors(); + Output_ObserveLevelLoad(); Render_Reset( RENDER_RESET_PALETTE | RENDER_RESET_TEXTURES | RENDER_RESET_UVS); @@ -294,13 +294,7 @@ bool Level_Load(const GF_LEVEL *const level) Audio_Sample_CloseAll(); Audio_Sample_UnloadAll(); - for (int32_t i = 0; i < O_NUMBER_OF; i++) { - Object_Get(i)->loaded = false; - } - for (int32_t i = 0; i < MAX_STATIC_OBJECTS; i++) { - Object_Get2DStatic(i)->loaded = false; - Object_Get3DStatic(i)->loaded = false; - } + Object_Reset(); Inject_InitLevel(level); @@ -377,14 +371,7 @@ bool Level_Initialise( void Level_Unload(void) { - strcpy(g_LevelFileName, ""); - Output_InitialiseTexturePages(0, true); - Output_InitialiseObjectTextures(0); - - if (Output_GetBackgroundType() == BK_OBJECT) { - Output_UnloadBackground(); - } - + Output_ObserveLevelUnload(); Camera_Reset(); } diff --git a/src/tr2/game/level.h b/src/tr2/game/level.h index d42377166..8ab62b911 100644 --- a/src/tr2/game/level.h +++ b/src/tr2/game/level.h @@ -2,7 +2,7 @@ #include "game/game_flow/types.h" -#include +#include void Level_Init(void); bool Level_Initialise(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx); diff --git a/src/tr2/game/los.c b/src/tr2/game/los.c index 12d38ffa4..caba2eb80 100644 --- a/src/tr2/game/los.c +++ b/src/tr2/game/los.c @@ -253,7 +253,7 @@ int32_t LOS_ClipTarget( return 1; } -int32_t LOS_Check(const GAME_VECTOR *const start, GAME_VECTOR *const target) +bool LOS_Check(const GAME_VECTOR *const start, GAME_VECTOR *const target) { int32_t los1; int32_t los2; @@ -270,7 +270,7 @@ int32_t LOS_Check(const GAME_VECTOR *const start, GAME_VECTOR *const target) } if (!los2) { - return 0; + return false; } if (dx == 0 && dz == 0) { @@ -281,12 +281,12 @@ int32_t LOS_Check(const GAME_VECTOR *const start, GAME_VECTOR *const target) Room_GetSector(target->x, target->y, target->z, &target->room_num); if (!LOS_ClipTarget(start, target, sector)) { - return 0; + return false; } if (los1 == 1 && los2 == 1) { - return 1; + return true; } - return 0; + return false; } int32_t LOS_CheckSmashable( diff --git a/src/tr2/game/los.h b/src/tr2/game/los.h index 7db997536..23d48a723 100644 --- a/src/tr2/game/los.h +++ b/src/tr2/game/los.h @@ -1,10 +1,10 @@ #pragma once -#include "global/types.h" +#include +#include int32_t LOS_CheckX(const GAME_VECTOR *start, GAME_VECTOR *target); int32_t LOS_CheckZ(const GAME_VECTOR *start, GAME_VECTOR *target); int32_t LOS_ClipTarget( const GAME_VECTOR *start, GAME_VECTOR *target, const SECTOR *sector); -int32_t LOS_Check(const GAME_VECTOR *start, GAME_VECTOR *target); int32_t LOS_CheckSmashable(const GAME_VECTOR *start, const GAME_VECTOR *target); diff --git a/src/tr2/game/lot.c b/src/tr2/game/lot.c index 5ff0cc34b..9a6e16e97 100644 --- a/src/tr2/game/lot.c +++ b/src/tr2/game/lot.c @@ -10,12 +10,13 @@ #include static int32_t m_SlotsUsed = 0; + void LOT_InitialiseArray(void) { g_BaddieSlots = - GameBuf_Alloc(NUM_SLOTS * sizeof(CREATURE), GBUF_CREATURE_DATA); + GameBuf_Alloc(LOT_SLOT_COUNT * sizeof(CREATURE), GBUF_CREATURE_DATA); - for (int32_t i = 0; i < NUM_SLOTS; i++) { + for (int32_t i = 0; i < LOT_SLOT_COUNT; i++) { CREATURE *const creature = &g_BaddieSlots[i]; creature->item_num = NO_ITEM; creature->lot.node = @@ -25,6 +26,11 @@ void LOT_InitialiseArray(void) m_SlotsUsed = 0; } +CREATURE *LOT_GetBaddieSlot(const int32_t i) +{ + return &g_BaddieSlots[i]; +} + void LOT_DisableBaddieAI(const int16_t item_num) { CREATURE *creature; @@ -54,8 +60,8 @@ bool LOT_EnableBaddieAI(const int16_t item_num, const bool always) return true; } - if (m_SlotsUsed < NUM_SLOTS) { - for (int32_t slot = 0; slot < NUM_SLOTS; slot++) { + if (m_SlotsUsed < LOT_SLOT_COUNT) { + for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) { if (g_BaddieSlots[slot].item_num == NO_ITEM) { LOT_InitialiseSlot(item_num, slot); return true; @@ -74,7 +80,7 @@ bool LOT_EnableBaddieAI(const int16_t item_num, const bool always) } int32_t worst_slot = -1; - for (int32_t slot = 0; slot < NUM_SLOTS; slot++) { + for (int32_t slot = 0; slot < LOT_SLOT_COUNT; slot++) { const int32_t item_num = g_BaddieSlots[slot].item_num; const ITEM *const item = Item_Get(item_num); const int32_t dx = (item->pos.x - g_Camera.pos.pos.x) >> 8; diff --git a/src/tr2/game/lot.h b/src/tr2/game/lot.h index 5bd72360f..1976b2cf7 100644 --- a/src/tr2/game/lot.h +++ b/src/tr2/game/lot.h @@ -5,7 +5,5 @@ #include void LOT_InitialiseArray(void); -void LOT_DisableBaddieAI(int16_t item_num); void LOT_InitialiseSlot(int16_t item_num, int32_t slot); void LOT_CreateZone(ITEM *item); -void LOT_ClearLOT(LOT_INFO *LOT); diff --git a/src/tr2/game/objects/common.c b/src/tr2/game/objects/common.c index 8c3688289..20ecc6366 100644 --- a/src/tr2/game/objects/common.c +++ b/src/tr2/game/objects/common.c @@ -1,13 +1,13 @@ #include "game/objects/common.h" #include "game/items.h" -#include "game/lara/misc.h" #include "game/output.h" #include "game/room.h" #include "game/viewport.h" #include "global/vars.h" #include +#include #include #include @@ -62,8 +62,12 @@ void Object_DrawUnclippedItem(const ITEM *const item) void Object_DrawSpriteItem(const ITEM *const item) { + SHADE shade = item->shade; + if (shade.value_1 < 0) { + shade.value_1 = SHADE_NEUTRAL; + } Output_CalculateStaticMeshLight( - item->interp.result.pos, item->shade, Room_Get(item->room_num)); + item->interp.result.pos, shade, Room_Get(item->room_num)); const OBJECT *const obj = Object_Get(item->object_id); @@ -72,7 +76,7 @@ void Object_DrawSpriteItem(const ITEM *const item) | SPRITE_SHADE, item->interp.result.pos.x, item->interp.result.pos.y, item->interp.result.pos.z, obj->mesh_idx - item->frame_num, - Output_GetLightAdder() + 4096, 0); + Output_GetLightAdder() + SHADE_NEUTRAL, 0); } void Object_Collision( @@ -89,7 +93,7 @@ void Object_Collision( } if (coll->enable_baddie_push) { - Lara_Push(item, lara_item, coll, false, true); + Lara_Push(item, coll, false, true); } } diff --git a/src/tr2/game/objects/creatures/bandit_1.c b/src/tr2/game/objects/creatures/bandit_1.c index 9318aefe5..5ffbd2b9e 100644 --- a/src/tr2/game/objects/creatures/bandit_1.c +++ b/src/tr2/game/objects/creatures/bandit_1.c @@ -70,8 +70,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; - Object_GetBone(obj, 8)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; + Object_GetBone(obj, 8)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/bandit_2.c b/src/tr2/game/objects/creatures/bandit_2.c index db697e98e..998d03a15 100644 --- a/src/tr2/game/objects/creatures/bandit_2.c +++ b/src/tr2/game/objects/creatures/bandit_2.c @@ -71,8 +71,8 @@ static void M_Setup2A(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; - Object_GetBone(obj, 8)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; + Object_GetBone(obj, 8)->rot.y = true; } static void M_Setup2B(OBJECT *const obj) @@ -100,8 +100,8 @@ static void M_Setup2B(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; - Object_GetBone(obj, 8)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; + Object_GetBone(obj, 8)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/barracuda.c b/src/tr2/game/objects/creatures/barracuda.c index b49fd9ee8..7899e8629 100644 --- a/src/tr2/game/objects/creatures/barracuda.c +++ b/src/tr2/game/objects/creatures/barracuda.c @@ -61,7 +61,7 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/bird_guardian.c b/src/tr2/game/objects/creatures/bird_guardian.c index 8db943b54..1d202c7f1 100644 --- a/src/tr2/game/objects/creatures/bird_guardian.c +++ b/src/tr2/game/objects/creatures/bird_guardian.c @@ -6,6 +6,7 @@ #include "global/const.h" #include "global/vars.h" +#include #include // clang-format off @@ -17,7 +18,6 @@ #define BIRD_GUARDIAN_ATTACK_1_RANGE SQUARE(WALL_L) // = 1048576 #define BIRD_GUARDIAN_ATTACK_2_RANGE SQUARE(WALL_L * 2) // = 4194304 #define BIRD_GUARDIAN_PUNCH_DAMAGE 200 -#define BIRD_GUARDIAN_DEATH_FRAME 158 // clang-format on typedef enum { @@ -65,6 +65,9 @@ static void M_Setup(OBJECT *const obj) obj->hit_points = BIRD_GUARDIAN_HITPOINTS; obj->radius = BIRD_GUARDIAN_RADIUS; + if (g_Config.visuals.fix_texture_issues) { + obj->shadow_size = UNIT_SHADOW / 2; + } obj->intelligent = 1; obj->save_position = 1; @@ -72,7 +75,7 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 14)->rot_y = true; + Object_GetBone(obj, 14)->rot.y = true; } static void M_Control(const int16_t item_num) @@ -187,15 +190,9 @@ static void M_Control(const int16_t item_num) default: break; } - } else { - if (item->current_anim_state != BIRD_GUARDIAN_STATE_DEATH) { - Item_SwitchToAnim(item, BIRD_GUARDIAN_ANIM_DEATH, 0); - item->current_anim_state = BIRD_GUARDIAN_STATE_DEATH; - } - - if (Item_TestFrameEqual(item, BIRD_GUARDIAN_DEATH_FRAME)) { - g_LevelComplete = true; - } + } else if (item->current_anim_state != BIRD_GUARDIAN_STATE_DEATH) { + Item_SwitchToAnim(item, BIRD_GUARDIAN_ANIM_DEATH, 0); + item->current_anim_state = BIRD_GUARDIAN_STATE_DEATH; } Creature_Head(item, head); diff --git a/src/tr2/game/objects/creatures/cultist_1.c b/src/tr2/game/objects/creatures/cultist_1.c index a48de2dc6..0907c8bf7 100644 --- a/src/tr2/game/objects/creatures/cultist_1.c +++ b/src/tr2/game/objects/creatures/cultist_1.c @@ -72,7 +72,7 @@ static void M_Setup1(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; } static void M_Setup1A(OBJECT *const obj) @@ -101,7 +101,7 @@ static void M_Setup1A(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; } static void M_Setup1B(OBJECT *const obj) @@ -130,7 +130,7 @@ static void M_Setup1B(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 0)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; } static void M_Initialise(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/cultist_2.c b/src/tr2/game/objects/creatures/cultist_2.c index 6eedc04a4..15d827265 100644 --- a/src/tr2/game/objects/creatures/cultist_2.c +++ b/src/tr2/game/objects/creatures/cultist_2.c @@ -68,8 +68,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 0)->rot_y = true; - Object_GetBone(obj, 8)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; + Object_GetBone(obj, 8)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/cultist_3.c b/src/tr2/game/objects/creatures/cultist_3.c index ea32c5059..733f8e2da 100644 --- a/src/tr2/game/objects/creatures/cultist_3.c +++ b/src/tr2/game/objects/creatures/cultist_3.c @@ -276,10 +276,10 @@ static void M_Control(const int16_t item_num) Creature_Tilt(item, tilt); const OBJECT *const obj = Object_Get(item->object_id); - Object_GetBone(obj, 0)->rot_y = body != 0; - Object_GetBone(obj, 2)->rot_y = left != 0; - Object_GetBone(obj, 6)->rot_y = right != 0; - Object_GetBone(obj, 10)->rot_y = head != 0; + Object_GetBone(obj, 0)->rot.y = body != 0; + Object_GetBone(obj, 2)->rot.y = left != 0; + Object_GetBone(obj, 6)->rot.y = right != 0; + Object_GetBone(obj, 10)->rot.y = head != 0; if (body != 0) { Creature_Head(item, body); diff --git a/src/tr2/game/objects/creatures/diver.c b/src/tr2/game/objects/creatures/diver.c index 6cf5eec1a..6145144a1 100644 --- a/src/tr2/game/objects/creatures/diver.c +++ b/src/tr2/game/objects/creatures/diver.c @@ -115,8 +115,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 10)->rot_y = true; - Object_GetBone(obj, 14)->rot_z = true; + Object_GetBone(obj, 10)->rot.y = true; + Object_GetBone(obj, 14)->rot.z = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/dog.c b/src/tr2/game/objects/creatures/dog.c index 9917e9fb6..1b22857e1 100644 --- a/src/tr2/game/objects/creatures/dog.c +++ b/src/tr2/game/objects/creatures/dog.c @@ -77,7 +77,7 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 19)->rot_y = true; + Object_GetBone(obj, 19)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/dragon.c b/src/tr2/game/objects/creatures/dragon.c index bc09a2549..15bf0d611 100644 --- a/src/tr2/game/objects/creatures/dragon.c +++ b/src/tr2/game/objects/creatures/dragon.c @@ -2,18 +2,19 @@ #include "game/creature.h" #include "game/input.h" #include "game/lara/control.h" -#include "game/lara/misc.h" #include "game/lot.h" #include "game/objects/common.h" #include "game/output.h" #include "game/random.h" #include "game/sound.h" #include "game/spawn.h" +#include "game/stats.h" #include "global/const.h" #include "global/vars.h" #include #include +#include #include // clang-format off @@ -81,7 +82,7 @@ static void M_MarkDragonDead(const ITEM *const dragon_back_item) const ITEM *const dragon_front_item = Item_Get(dragon_front_item_num); CREATURE *const creature = dragon_front_item->data; creature->flags = -1; - g_SaveGame.current_stats.kills++; + Stats_AddKill(); } static void M_PushLaraAway( @@ -184,7 +185,7 @@ static void M_SetupFront(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 10)->rot_z = true; + Object_GetBone(obj, 10)->rot.z = true; } static void M_SetupBack(OBJECT *const obj) @@ -228,7 +229,7 @@ static void M_Collision( } if (item->current_anim_state != DRAGON_STATE_DEATH) { - Lara_Push(item, lara_item, coll, true, false); + Lara_Push(item, coll, true, false); return; } @@ -239,7 +240,7 @@ static void M_Collision( const int32_t side_shift = (cy * dz + sy * dx) >> W2V_SHIFT; if (side_shift <= DRAGON_L_COL || side_shift >= DRAGON_R_COL) { - Lara_Push(item, lara_item, coll, true, false); + Lara_Push(item, coll, true, false); return; } diff --git a/src/tr2/game/objects/creatures/monk.c b/src/tr2/game/objects/creatures/monk.c index 28c9426f7..c74158323 100644 --- a/src/tr2/game/objects/creatures/monk.c +++ b/src/tr2/game/objects/creatures/monk.c @@ -70,7 +70,7 @@ static void M_SetupBase(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; } static void M_Setup2(OBJECT *const obj) @@ -113,7 +113,8 @@ static void M_Control(const int16_t item_num) switch (item->current_anim_state) { case MONK_STATE_WAIT_1: creature->flags &= 0xFFF; - if (!g_IsMonkAngry && info.ahead != 0 && g_Lara.target == item) { + if (!Creature_AreAlliesHostile() && info.ahead != 0 + && g_Lara.target == item) { } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = MONK_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { @@ -137,7 +138,8 @@ static void M_Control(const int16_t item_num) case MONK_STATE_WAIT_2: creature->flags &= 0xFFF; - if (!g_IsMonkAngry && info.ahead != 0 && g_Lara.target == item) { + if (!Creature_AreAlliesHostile() && info.ahead != 0 + && g_Lara.target == item) { } else if (creature->mood == MOOD_BORED) { item->goal_anim_state = MONK_STATE_WALK; } else if (creature->mood == MOOD_ESCAPE) { @@ -161,7 +163,7 @@ static void M_Control(const int16_t item_num) case MONK_STATE_WALK: creature->maximum_turn = MONK_WALK_TURN; if (creature->mood == MOOD_BORED) { - if (!g_IsMonkAngry && info.ahead != 0 + if (!Creature_AreAlliesHostile() && info.ahead != 0 && g_Lara.target == item) { if (Random_GetControl() < 0x4000) { item->goal_anim_state = MONK_STATE_WAIT_1; @@ -185,7 +187,7 @@ static void M_Control(const int16_t item_num) case MONK_STATE_RUN: creature->flags &= 0xFFF; creature->maximum_turn = MONK_RUN_TURN; - if (g_IsMonkAngry) { + if (Creature_AreAlliesHostile()) { creature->maximum_turn = MONK_RUN_TURN_FAST; } tilt = angle / 4; diff --git a/src/tr2/game/objects/creatures/mouse.c b/src/tr2/game/objects/creatures/mouse.c index 009f9db05..330968dc7 100644 --- a/src/tr2/game/objects/creatures/mouse.c +++ b/src/tr2/game/objects/creatures/mouse.c @@ -63,7 +63,7 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 3)->rot_y = true; + Object_GetBone(obj, 3)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/shark.c b/src/tr2/game/objects/creatures/shark.c index 530567ef2..c9ef7c129 100644 --- a/src/tr2/game/objects/creatures/shark.c +++ b/src/tr2/game/objects/creatures/shark.c @@ -70,7 +70,7 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 9)->rot_y = true; + Object_GetBone(obj, 9)->rot.y = true; } static void M_Control(const int16_t item_num) @@ -159,7 +159,7 @@ static void M_Control(const int16_t item_num) } if (lara_alive && g_LaraItem->hit_points <= 0) { - Creature_Kill( + Creature_SpecialKill( item, SHARK_ANIM_KILL, SHARK_STATE_KILL, LA_EXTRA_SHARK_KILL); } else if (item->current_anim_state == SHARK_STATE_KILL) { Item_Animate(item); diff --git a/src/tr2/game/objects/creatures/tiger.c b/src/tr2/game/objects/creatures/tiger.c index b10e2a664..9c2d87102 100644 --- a/src/tr2/game/objects/creatures/tiger.c +++ b/src/tr2/game/objects/creatures/tiger.c @@ -68,7 +68,7 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 21)->rot_y = true; + Object_GetBone(obj, 21)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/trex.c b/src/tr2/game/objects/creatures/trex.c index ad05df7db..294818b9d 100644 --- a/src/tr2/game/objects/creatures/trex.c +++ b/src/tr2/game/objects/creatures/trex.c @@ -63,8 +63,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 10)->rot_y = true; - Object_GetBone(obj, 11)->rot_y = true; + Object_GetBone(obj, 10)->rot.y = true; + Object_GetBone(obj, 11)->rot.y = true; } static void M_Control(const int16_t item_num) @@ -147,7 +147,7 @@ static void M_Control(const int16_t item_num) case TREX_STATE_ATTACK_2: if ((item->touch_bits & TREX_TOUCH_BITS) != 0) { Lara_TakeDamage(TREX_BITE_DAMAGE, true); - Creature_Kill( + Creature_SpecialKill( item, TREX_ANIM_KILL, TREX_STATE_KILL, LA_EXTRA_TREX_KILL); return; } diff --git a/src/tr2/game/objects/creatures/worker_1.c b/src/tr2/game/objects/creatures/worker_1.c index c52399904..3055d79a4 100644 --- a/src/tr2/game/objects/creatures/worker_1.c +++ b/src/tr2/game/objects/creatures/worker_1.c @@ -65,8 +65,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 4)->rot_y = true; - Object_GetBone(obj, 13)->rot_y = true; + Object_GetBone(obj, 4)->rot.y = true; + Object_GetBone(obj, 13)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/worker_2.c b/src/tr2/game/objects/creatures/worker_2.c index 6224f13b4..9a01abbfa 100644 --- a/src/tr2/game/objects/creatures/worker_2.c +++ b/src/tr2/game/objects/creatures/worker_2.c @@ -88,8 +88,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 4)->rot_y = true; - Object_GetBone(obj, 13)->rot_y = true; + Object_GetBone(obj, 4)->rot.y = true; + Object_GetBone(obj, 13)->rot.y = true; } static void M_Setup5(OBJECT *const obj) @@ -112,8 +112,8 @@ static void M_Setup5(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 4)->rot_y = true; - Object_GetBone(obj, 13)->rot_y = true; + Object_GetBone(obj, 4)->rot.y = true; + Object_GetBone(obj, 13)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/worker_3.c b/src/tr2/game/objects/creatures/worker_3.c index a5b0158e1..08aa83e25 100644 --- a/src/tr2/game/objects/creatures/worker_3.c +++ b/src/tr2/game/objects/creatures/worker_3.c @@ -83,8 +83,8 @@ static void M_SetupBase(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 0)->rot_y = true; - Object_GetBone(obj, 4)->rot_y = true; + Object_GetBone(obj, 0)->rot.y = true; + Object_GetBone(obj, 4)->rot.y = true; } static void M_Setup3(OBJECT *const obj) diff --git a/src/tr2/game/objects/creatures/xian_common.c b/src/tr2/game/objects/creatures/xian_common.c index 969fe5d94..c55296922 100644 --- a/src/tr2/game/objects/creatures/xian_common.c +++ b/src/tr2/game/objects/creatures/xian_common.c @@ -48,6 +48,7 @@ void XianWarrior_Draw(const ITEM *item) Matrix_Rot16_ID( frames[0]->mesh_rots[mesh_idx], frames[1]->mesh_rots[mesh_idx]); + Object_ApplyExtraRotation(&extra_rotation, obj->base_rot, true); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { @@ -61,17 +62,7 @@ void XianWarrior_Draw(const ITEM *item) Matrix_Rot16_ID( frames[0]->mesh_rots[mesh_idx], frames[1]->mesh_rots[mesh_idx]); - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY_I(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX_I(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ_I(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, true); } if (item->mesh_bits & (1 << mesh_idx)) { @@ -85,6 +76,8 @@ void XianWarrior_Draw(const ITEM *item) if (mesh_idx == 0) { Matrix_TranslateRel16(frames[0]->offset); Matrix_Rot16(frames[0]->mesh_rots[mesh_idx]); + Object_ApplyExtraRotation( + &extra_rotation, obj->base_rot, false); } else { const ANIM_BONE *const bone = Object_GetBone(obj, mesh_idx - 1); if (bone->matrix_pop) { @@ -96,17 +89,7 @@ void XianWarrior_Draw(const ITEM *item) Matrix_TranslateRel32(bone->pos); Matrix_Rot16(frames[0]->mesh_rots[mesh_idx]); - if (extra_rotation != nullptr) { - if (bone->rot_y) { - Matrix_RotY(*extra_rotation++); - } - if (bone->rot_x) { - Matrix_RotX(*extra_rotation++); - } - if (bone->rot_z) { - Matrix_RotZ(*extra_rotation++); - } - } + Object_ApplyExtraRotation(&extra_rotation, bone->rot, false); } if (item->mesh_bits & (1 << mesh_idx)) { diff --git a/src/tr2/game/objects/creatures/xian_knight.c b/src/tr2/game/objects/creatures/xian_knight.c index c4adc25bd..7ba224e1d 100644 --- a/src/tr2/game/objects/creatures/xian_knight.c +++ b/src/tr2/game/objects/creatures/xian_knight.c @@ -101,8 +101,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; - Object_GetBone(obj, 16)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; + Object_GetBone(obj, 16)->rot.y = true; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/creatures/xian_spearman.c b/src/tr2/game/objects/creatures/xian_spearman.c index ca9902dd7..aef49992e 100644 --- a/src/tr2/game/objects/creatures/xian_spearman.c +++ b/src/tr2/game/objects/creatures/xian_spearman.c @@ -127,8 +127,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; - Object_GetBone(obj, 12)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; + Object_GetBone(obj, 12)->rot.y = true; } static void M_Initialise(const int16_t item_num) @@ -409,7 +409,7 @@ static void M_Control(const int16_t item_num) } if (lara_alive && g_LaraItem->hit_points <= 0) { - Creature_Kill( + Creature_SpecialKill( item, XIAN_SPEARMAN_ANIM_KILL, XIAN_SPEARMAN_STATE_KILL, LA_EXTRA_YETI_KILL); return; diff --git a/src/tr2/game/objects/creatures/yeti.c b/src/tr2/game/objects/creatures/yeti.c index 92962c75b..fa5e67c17 100644 --- a/src/tr2/game/objects/creatures/yeti.c +++ b/src/tr2/game/objects/creatures/yeti.c @@ -91,8 +91,8 @@ static void M_Setup(OBJECT *const obj) obj->save_flags = 1; obj->save_anim = 1; - Object_GetBone(obj, 6)->rot_y = true; - Object_GetBone(obj, 14)->rot_y = true; + Object_GetBone(obj, 6)->rot.y = true; + Object_GetBone(obj, 14)->rot.y = true; } static void M_Control(const int16_t item_num) @@ -284,7 +284,7 @@ static void M_Control(const int16_t item_num) } if (lara_alive && g_LaraItem->hit_points <= 0) { - Creature_Kill( + Creature_SpecialKill( item, YETI_ANIM_KILL, YETI_STATE_KILL, LA_EXTRA_YETI_KILL); return; } diff --git a/src/tr2/game/objects/effects/body_part.c b/src/tr2/game/objects/effects/body_part.c index 51d7e2435..85dca0a8f 100644 --- a/src/tr2/game/objects/effects/body_part.c +++ b/src/tr2/game/objects/effects/body_part.c @@ -62,7 +62,7 @@ static void M_Control(const int16_t effect_num) effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION; - effect->shade = HIGH_LIGHT; + effect->shade = SHADE_NEUTRAL; Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); } else { Effect_Kill(effect_num); @@ -78,7 +78,7 @@ static void M_Control(const int16_t effect_num) effect->frame_num = 0; effect->counter = 0; effect->object_id = O_EXPLOSION; - effect->shade = HIGH_LIGHT; + effect->shade = SHADE_NEUTRAL; Sound_Effect(SFX_EXPLOSION_1, &effect->pos, SPM_NORMAL); g_Lara.hit_effect_count = 5; g_Lara.hit_effect = effect; diff --git a/src/tr2/game/objects/general/bridge_common.c b/src/tr2/game/objects/general/bridge_common.c deleted file mode 100644 index 396fef560..000000000 --- a/src/tr2/game/objects/general/bridge_common.c +++ /dev/null @@ -1,16 +0,0 @@ -#include "game/objects/general/bridge_common.h" - -int32_t Bridge_GetOffset( - const ITEM *const item, const int32_t x, const int32_t z) -{ - switch (item->rot.y) { - case 0: - return (WALL_L - x) & (WALL_L - 1); - case -DEG_180: - return x & (WALL_L - 1); - case DEG_90: - return z & (WALL_L - 1); - default: - return (WALL_L - z) & (WALL_L - 1); - } -} diff --git a/src/tr2/game/objects/general/bridge_common.h b/src/tr2/game/objects/general/bridge_common.h deleted file mode 100644 index da5090619..000000000 --- a/src/tr2/game/objects/general/bridge_common.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "global/types.h" - -int32_t Bridge_GetOffset(const ITEM *item, int32_t x, int32_t z); diff --git a/src/tr2/game/objects/general/bridge_flat.c b/src/tr2/game/objects/general/bridge_flat.c deleted file mode 100644 index fe26b8543..000000000 --- a/src/tr2/game/objects/general/bridge_flat.c +++ /dev/null @@ -1,35 +0,0 @@ -#include "game/objects/general/bridge_common.h" - -static int16_t M_GetFloorHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static int16_t M_GetCeilingHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static void M_Setup(OBJECT *obj); - -static int16_t M_GetFloorHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - if (y > item->pos.y) { - return height; - } - return item->pos.y; -} - -static int16_t M_GetCeilingHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - if (y <= item->pos.y) { - return height; - } - return item->pos.y + STEP_L; -} - -static void M_Setup(OBJECT *const obj) -{ - obj->floor_height_func = M_GetFloorHeight; - obj->ceiling_height_func = M_GetCeilingHeight; -} - -REGISTER_OBJECT(O_BRIDGE_FLAT, M_Setup) diff --git a/src/tr2/game/objects/general/bridge_tilt_1.c b/src/tr2/game/objects/general/bridge_tilt_1.c deleted file mode 100644 index 23ae126a8..000000000 --- a/src/tr2/game/objects/general/bridge_tilt_1.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "game/objects/general/bridge_common.h" - -static int16_t M_GetFloorHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static int16_t M_GetCeilingHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static void M_Setup(OBJECT *obj); - -static int16_t M_GetFloorHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - const int32_t offset_height = - item->pos.y + (Bridge_GetOffset(item, x, z) / 4); - if (y > offset_height) { - return height; - } - return offset_height; -} - -static int16_t M_GetCeilingHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - const int32_t offset_height = - item->pos.y + (Bridge_GetOffset(item, x, z) / 4); - if (y <= offset_height) { - return height; - } - return offset_height + STEP_L; -} - -static void M_Setup(OBJECT *const obj) -{ - obj->floor_height_func = M_GetFloorHeight; - obj->ceiling_height_func = M_GetCeilingHeight; -} - -REGISTER_OBJECT(O_BRIDGE_TILT_1, M_Setup) diff --git a/src/tr2/game/objects/general/bridge_tilt_2.c b/src/tr2/game/objects/general/bridge_tilt_2.c deleted file mode 100644 index c5ba54e07..000000000 --- a/src/tr2/game/objects/general/bridge_tilt_2.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "game/objects/general/bridge_common.h" - -static int16_t M_GetFloorHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static int16_t M_GetCeilingHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static void M_Setup(OBJECT *obj); - -static int16_t M_GetFloorHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - const int32_t offset_height = - item->pos.y + (Bridge_GetOffset(item, x, z) / 2); - if (y > offset_height) { - return height; - } - return offset_height; -} - -static int16_t M_GetCeilingHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - const int32_t offset_height = - item->pos.y + (Bridge_GetOffset(item, x, z) / 2); - if (y <= offset_height) { - return height; - } - return offset_height + STEP_L; -} - -static void M_Setup(OBJECT *const obj) -{ - obj->floor_height_func = M_GetFloorHeight; - obj->ceiling_height_func = M_GetCeilingHeight; -} - -REGISTER_OBJECT(O_BRIDGE_TILT_2, M_Setup) diff --git a/src/tr2/game/objects/general/door.c b/src/tr2/game/objects/general/door.c index b7d6eed03..819c8fb5f 100644 --- a/src/tr2/game/objects/general/door.c +++ b/src/tr2/game/objects/general/door.c @@ -1,14 +1,13 @@ -#include "game/objects/general/door.h" - #include "game/box.h" #include "game/items.h" -#include "game/lara/misc.h" #include "game/objects/common.h" #include "game/room.h" #include "global/vars.h" #include #include +#include +#include typedef struct { DOORPOS_DATA d1; @@ -206,7 +205,7 @@ void Door_Collision( if (coll->enable_baddie_push) { Lara_Push( - item, lara_item, coll, + item, coll, item->current_anim_state != item->goal_anim_state ? coll->enable_hit : false, true); diff --git a/src/tr2/game/objects/general/door.h b/src/tr2/game/objects/general/door.h deleted file mode 100644 index c702447b8..000000000 --- a/src/tr2/game/objects/general/door.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "global/types.h" - -typedef enum { - DOOR_STATE_CLOSED = 0, - DOOR_STATE_OPEN = 1, -} DOOR_STATE; - -void Door_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); diff --git a/src/tr2/game/objects/general/drawbridge.c b/src/tr2/game/objects/general/drawbridge.c deleted file mode 100644 index d891f8b3d..000000000 --- a/src/tr2/game/objects/general/drawbridge.c +++ /dev/null @@ -1,99 +0,0 @@ -#include "game/objects/general/door.h" -#include "game/objects/general/general.h" - -typedef enum { - DRAWBRIDGE_STATE_CLOSED = DOOR_STATE_CLOSED, - DRAWBRIDGE_STATE_OPEN = DOOR_STATE_OPEN, -} DRAWBRIDGE_STATE; - -static int16_t M_GetFloorHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static int16_t M_GetCeilingHeight( - const ITEM *item, int32_t x, int32_t y, int32_t z, int16_t height); -static bool M_IsItemOnTop(const ITEM *item, int32_t z, int32_t x); -static void M_Setup(OBJECT *obj); -static void M_Collision(int16_t item_num, ITEM *lara_item, COLL_INFO *coll); - -static int16_t M_GetFloorHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - if (item->current_anim_state != DRAWBRIDGE_STATE_OPEN) { - return height; - } else if (!M_IsItemOnTop(item, z, x)) { - return height; - } else if (item->pos.y < y) { - return height; - } - return item->pos.y; -} - -static int16_t M_GetCeilingHeight( - const ITEM *const item, const int32_t x, const int32_t y, const int32_t z, - const int16_t height) -{ - if (item->current_anim_state != DRAWBRIDGE_STATE_OPEN) { - return height; - } else if (!M_IsItemOnTop(item, z, x)) { - return height; - } else if (item->pos.y >= y) { - return height; - } - return item->pos.y + STEP_L; -} - -static bool M_IsItemOnTop( - const ITEM *const item, const int32_t z, const int32_t x) -{ - // drawbridge sector - const XZ_32 obj = { - .x = item->pos.x >> WALL_SHIFT, - .z = item->pos.z >> WALL_SHIFT, - }; - - // test sector - const XZ_32 test = { - .x = x >> WALL_SHIFT, - .z = z >> WALL_SHIFT, - }; - - switch (item->rot.y) { - case 0: - return test.x == obj.x && (test.z == obj.z - 1 || test.z == obj.z - 2); - - case -DEG_180: - return test.x == obj.x && (test.z == obj.z + 1 || test.z == obj.z + 2); - - case -DEG_90: - return test.z == obj.z && (test.x == obj.x + 1 || test.x == obj.x + 2); - - case DEG_90: - return test.z == obj.z && (test.x == obj.x - 1 || test.x == obj.x - 2); - } - - return false; -} - -static void M_Setup(OBJECT *const obj) -{ - if (!obj->loaded) { - return; - } - obj->control_func = General_Control; - obj->collision_func = M_Collision; - obj->floor_height_func = M_GetFloorHeight; - obj->ceiling_height_func = M_GetCeilingHeight; - obj->save_flags = 1; - obj->save_anim = 1; -} - -static void M_Collision( - const int16_t item_num, ITEM *const lara_item, COLL_INFO *const coll) -{ - const ITEM *const item = Item_Get(item_num); - if (item->current_anim_state == DRAWBRIDGE_STATE_CLOSED) { - Door_Collision(item_num, lara_item, coll); - } -} - -REGISTER_OBJECT(O_DRAWBRIDGE, M_Setup) diff --git a/src/tr2/game/objects/general/final_level_counter.c b/src/tr2/game/objects/general/final_level_counter.c index 0694ceb2f..9672bc0b2 100644 --- a/src/tr2/game/objects/general/final_level_counter.c +++ b/src/tr2/game/objects/general/final_level_counter.c @@ -1,11 +1,13 @@ #include "decomp/flares.h" #include "game/camera.h" #include "game/creature.h" +#include "game/game.h" #include "game/gun/gun.h" #include "game/items.h" #include "game/los.h" #include "game/lot.h" #include "game/objects/common.h" +#include "game/savegame.h" #include "global/vars.h" #include @@ -76,7 +78,7 @@ static void M_PrepareCutscene(const int16_t item_num) g_Lara.water_status = LWS_ABOVE_WATER; ITEM *const item = Item_Get(item_num); - Creature_Kill(item, 0, 0, LA_EXTRA_FINAL_ANIM); + Creature_SpecialKill(item, 0, 0, LA_EXTRA_FINAL_ANIM); Camera_InvokeCinematic(item, 428, 0); } @@ -90,13 +92,15 @@ static void M_Setup(OBJECT *const obj) static void M_Control(const int16_t item_num) { - if (g_SaveGame.current_stats.kills == g_FinalLevelCount + const RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + if (current_info->stats.kill_count == g_FinalLevelCount && !g_FinalBossActive) { M_ActivateLastBoss(); return; } - if (g_SaveGame.current_stats.kills > g_FinalLevelCount) { + if (current_info->stats.kill_count > g_FinalLevelCount) { g_FinalBossActive++; if (g_FinalBossActive == CUTSCENE_DELAY) { M_PrepareCutscene(item_num); diff --git a/src/tr2/game/objects/general/grenade.c b/src/tr2/game/objects/general/grenade.c index 631c51c60..94cdb0bf9 100644 --- a/src/tr2/game/objects/general/grenade.c +++ b/src/tr2/game/objects/general/grenade.c @@ -5,6 +5,7 @@ #include "game/objects/general/window.h" #include "game/room.h" #include "game/sound.h" +#include "game/stats.h" #include "global/vars.h" #include @@ -130,7 +131,7 @@ static void M_Control(const int16_t item_num) } Gun_HitTarget(target_item, nullptr, 30); - g_SaveGame.current_stats.ammo_hits++; + Stats_AddAmmoHits(); if (target_item->hit_points <= 0) { if (target_item->object_id != O_DRAGON_FRONT diff --git a/src/tr2/game/objects/general/harpoon_bolt.c b/src/tr2/game/objects/general/harpoon_bolt.c index 1c33e628c..2e0922c0a 100644 --- a/src/tr2/game/objects/general/harpoon_bolt.c +++ b/src/tr2/game/objects/general/harpoon_bolt.c @@ -3,6 +3,7 @@ #include "game/objects/general/window.h" #include "game/room.h" #include "game/spawn.h" +#include "game/stats.h" #include "global/vars.h" #include @@ -61,6 +62,9 @@ static void M_Control(const int16_t item_num) } const ANIM_FRAME *const frame = Item_GetBestFrame(target_item); + if (frame == nullptr) { + continue; + } const BOUNDS_16 *const bounds = &frame->bounds; const int32_t cdy = item->pos.y - target_item->pos.y; @@ -98,7 +102,7 @@ static void M_Control(const int16_t item_num) 5); Gun_HitTarget( target_item, nullptr, g_Weapons[LGT_HARPOON].damage); - g_SaveGame.current_stats.ammo_hits++; + Stats_AddAmmoHits(); } Item_Kill(item_num); return; diff --git a/src/tr2/game/objects/general/lift.c b/src/tr2/game/objects/general/lift.c index 49bf5b6d2..0177c4244 100644 --- a/src/tr2/game/objects/general/lift.c +++ b/src/tr2/game/objects/general/lift.c @@ -5,6 +5,7 @@ #include "global/vars.h" #include +#include #define LIFT_WAIT_TIME (3 * FRAMES_PER_SECOND) // = 90 #define LIFT_SHIFT 16 @@ -46,14 +47,38 @@ static void M_FloorCeiling( .z = z >> WALL_SHIFT, }; + const DIRECTION direction = Math_GetDirection(item->rot.y); + int32_t dx = 0; + int32_t dz = 0; + switch (direction) { + case DIR_NORTH: + dx = -1; + dz = 1; + break; + case DIR_EAST: + dx = 1; + dz = 1; + break; + case DIR_SOUTH: + dx = 1; + dz = -1; + break; + case DIR_WEST: + dx = -1; + dz = -1; + break; + default: + break; + } + // clang-format off const bool point_in_shaft = - (test_tile.x == lift_tile.x || test_tile.x + 1 == lift_tile.x) && - (test_tile.z == lift_tile.z || test_tile.z - 1 == lift_tile.z); + (test_tile.x == lift_tile.x || test_tile.x + dx == lift_tile.x) && + (test_tile.z == lift_tile.z || test_tile.z + dz == lift_tile.z); const bool lara_in_shaft = - (lara_tile.x == lift_tile.x || lara_tile.x + 1 == lift_tile.x) && - (lara_tile.z == lift_tile.z || lara_tile.z - 1 == lift_tile.z); + (lara_tile.x == lift_tile.x || lara_tile.x + dx == lift_tile.x) && + (lara_tile.z == lift_tile.z || lara_tile.z + dz == lift_tile.z); // clang-format on const int32_t lift_floor = item->pos.y; diff --git a/src/tr2/game/objects/general/movable_block.c b/src/tr2/game/objects/general/movable_block.c index 6e00b5234..63f0f1b2b 100644 --- a/src/tr2/game/objects/general/movable_block.c +++ b/src/tr2/game/objects/general/movable_block.c @@ -203,6 +203,7 @@ static void M_Setup(OBJECT *const obj) obj->save_position = 1; obj->save_flags = 1; obj->save_anim = 1; + obj->base_rot.y = true; } static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) @@ -214,6 +215,7 @@ static void M_HandleSave(ITEM *const item, const SAVEGAME_STAGE stage) item->status = IS_INACTIVE; } item->priv = item->status == IS_ACTIVE ? (void *)true : (void *)false; + MovableBlock_UpdateRotation(item, item->rot.y); } } @@ -286,16 +288,16 @@ static void M_Collision( switch (quadrant) { case DIR_NORTH: - item->rot.y = 0; + MovableBlock_UpdateRotation(item, 0); break; case DIR_EAST: - item->rot.y = DEG_90; + MovableBlock_UpdateRotation(item, DEG_90); break; case DIR_SOUTH: - item->rot.y = -DEG_180; + MovableBlock_UpdateRotation(item, -DEG_180); break; case DIR_WEST: - item->rot.y = -DEG_90; + MovableBlock_UpdateRotation(item, -DEG_90); break; default: break; diff --git a/src/tr2/game/objects/general/waterfall.c b/src/tr2/game/objects/general/waterfall.c index b2446b3c1..c84f9f24a 100644 --- a/src/tr2/game/objects/general/waterfall.c +++ b/src/tr2/game/objects/general/waterfall.c @@ -17,6 +17,7 @@ static void M_Setup(OBJECT *const obj) { obj->control_func = M_Control; obj->draw_func = Object_DrawDummyItem; + obj->save_flags = 1; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/traps/gondola.c b/src/tr2/game/objects/traps/gondola.c index c098d3aa4..c74c6cb35 100644 --- a/src/tr2/game/objects/traps/gondola.c +++ b/src/tr2/game/objects/traps/gondola.c @@ -15,6 +15,7 @@ static void M_Setup(OBJECT *const obj) obj->collision_func = Object_Collision; obj->save_flags = 1; obj->save_anim = 1; + obj->save_position = 1; } static void M_Control(const int16_t item_num) diff --git a/src/tr2/game/objects/traps/mine.c b/src/tr2/game/objects/traps/mine.c index 42b8713a5..3487225e4 100644 --- a/src/tr2/game/objects/traps/mine.c +++ b/src/tr2/game/objects/traps/mine.c @@ -48,8 +48,12 @@ static void M_DetonateAll( g_LaraItem->flags |= IF_ONE_SHOT; } - boat_item->object_id = O_BOAT_BITS; - Item_Explode(boat_item_num, -1, 0); + const OBJECT *const obj = Object_Get(O_BOAT_BITS); + if (obj->loaded) { + boat_item->object_id = O_BOAT_BITS; + boat_item->mesh_bits = (1 << obj->mesh_count) - 1; + Item_Explode(boat_item_num, -1, 0); + } Item_Kill(boat_item_num); boat_item->object_id = O_BOAT; diff --git a/src/tr2/game/objects/traps/rolling_ball.c b/src/tr2/game/objects/traps/rolling_ball.c index 627798ffb..3b7847d17 100644 --- a/src/tr2/game/objects/traps/rolling_ball.c +++ b/src/tr2/game/objects/traps/rolling_ball.c @@ -1,6 +1,5 @@ #include "game/camera.h" #include "game/items.h" -#include "game/lara/misc.h" #include "game/objects/common.h" #include "game/random.h" #include "game/room.h" @@ -10,6 +9,7 @@ #include #include +#include #include #include @@ -167,7 +167,7 @@ static void M_Collision( if (lara_item->gravity) { if (coll->enable_baddie_push) { - Lara_Push(item, lara_item, coll, coll->enable_hit, true); + Lara_Push(item, coll, coll->enable_hit, true); } lara_item->hit_points -= ROLLING_BALL_DAMAGE_AIR; diff --git a/src/tr2/game/objects/traps/spike_wall.c b/src/tr2/game/objects/traps/spike_wall.c index 9a3784a4c..fa6a8ceee 100644 --- a/src/tr2/game/objects/traps/spike_wall.c +++ b/src/tr2/game/objects/traps/spike_wall.c @@ -6,16 +6,30 @@ #include "game/spawn.h" #include "global/vars.h" +#include #include #define SPIKE_WALL_DAMAGE 20 #define SPIKE_WALL_SPEED 1 +static void M_Initialise(int16_t item_num); static void M_Move(int16_t item_num); +static void M_Reset(int16_t item_num); static void M_HitLara(ITEM *item); static void M_Setup(OBJECT *obj); static void M_Control(int16_t item_num); +static void M_Initialise(const int16_t item_num) +{ + ITEM *const item = Item_Get(item_num); + + GAME_VECTOR *const data = + GameBuf_Alloc(sizeof(GAME_VECTOR), GBUF_ITEM_DATA); + data->pos = item->pos; + data->room_num = item->room_num; + item->data = data; +} + static void M_Move(const int16_t item_num) { ITEM *const item = Item_Get(item_num); @@ -25,9 +39,9 @@ static void M_Move(const int16_t item_num) item->pos.x + (SPIKE_WALL_SPEED * Math_Sin(item->rot.y) >> WALL_SHIFT); int16_t room_num = item->room_num; - const SECTOR *const Sector = Room_GetSector(x, item->pos.y, z, &room_num); + const SECTOR *const sector = Room_GetSector(x, item->pos.y, z, &room_num); - if (Room_GetHeight(Sector, x, item->pos.y, z) != item->pos.y) { + if (Room_GetHeight(sector, x, item->pos.y, z) != item->pos.y) { item->status = IS_DEACTIVATED; } else { item->pos.z = z; @@ -40,6 +54,24 @@ static void M_Move(const int16_t item_num) Sound_Effect(SFX_DOOR_SLIDE, &item->pos, SPM_NORMAL); } +static void M_Reset(const int16_t item_num) +{ + ITEM *const item = Item_Get(item_num); + item->status = IS_INACTIVE; + + const GAME_VECTOR *const data = item->data; + item->pos = data->pos; + if (item->room_num != data->room_num) { + Item_RemoveDrawn(item_num); + ROOM *const room = Room_Get(data->room_num); + item->next_item = room->item_num; + room->item_num = item_num; + item->room_num = data->room_num; + } + + Item_RemoveActive(item_num); +} + static void M_HitLara(ITEM *const item) { Lara_TakeDamage(SPIKE_WALL_DAMAGE, true); @@ -54,6 +86,7 @@ static void M_HitLara(ITEM *const item) static void M_Setup(OBJECT *const obj) { + obj->initialise_func = M_Initialise; obj->control_func = M_Control; obj->collision_func = Object_Collision; obj->save_position = 1; @@ -64,7 +97,9 @@ static void M_Control(const int16_t item_num) { ITEM *const item = Item_Get(item_num); - if (Item_IsTriggerActive(item) && item->status != IS_DEACTIVATED) { + if (!Item_IsTriggerActive(item)) { + M_Reset(item_num); + } else if (item->status != IS_DEACTIVATED) { M_Move(item_num); } diff --git a/src/tr2/game/objects/traps/springboard.c b/src/tr2/game/objects/traps/springboard.c index 568ab69c8..f181329bf 100644 --- a/src/tr2/game/objects/traps/springboard.c +++ b/src/tr2/game/objects/traps/springboard.c @@ -35,17 +35,29 @@ static void M_Control(const int16_t item_num) return; } - if (lara_item->current_anim_state == LS_BACK - || lara_item->current_anim_state == LS_FAST_BACK) { - lara_item->speed = -lara_item->speed; + const LARA_INFO *const lara = Lara_GetLaraInfo(); + if (lara->skidoo != NO_ITEM) { + ITEM *const skidoo = Item_Get(lara->skidoo); + if (skidoo->object_id != O_SKIDOO_FAST + && skidoo->object_id != O_SKIDOO_ARMED) { + return; + } + + skidoo->fall_speed = -200; + skidoo->pos.y -= STEP_L; + } else { + if (lara_item->current_anim_state == LS_BACK + || lara_item->current_anim_state == LS_FAST_BACK) { + lara_item->speed = -lara_item->speed; + } + + lara_item->fall_speed = -240; + lara_item->gravity = 1; + + Item_SwitchToAnim(lara_item, LA_FALL_START, 0); + lara_item->current_anim_state = LS_JUMP_FORWARD; + lara_item->goal_anim_state = LS_JUMP_FORWARD; } - - lara_item->fall_speed = -240; - lara_item->gravity = 1; - - Item_SwitchToAnim(lara_item, LA_FALL_START, 0); - lara_item->current_anim_state = LS_JUMP_FORWARD; - lara_item->goal_anim_state = LS_JUMP_FORWARD; item->goal_anim_state = SPRINGBOARD_STATE_ON; } diff --git a/src/tr2/game/objects/vars.c b/src/tr2/game/objects/vars.c index 0a75ba861..6719ddbb2 100644 --- a/src/tr2/game/objects/vars.c +++ b/src/tr2/game/objects/vars.c @@ -242,6 +242,14 @@ const GAME_OBJECT_ID g_InvObjects[] = { // clang-format on }; +const GAME_OBJECT_ID g_WaterSpriteObjects[] = { + // clang-format off + O_WATERFALL, + O_SPLASH, + O_BUBBLE, + // clang-format on +}; + const GAME_OBJECT_PAIR g_ItemToInvObjectMap[] = { // clang-format off { O_COMPASS_ITEM, O_COMPASS_OPTION }, diff --git a/src/tr2/game/objects/vehicles/boat.c b/src/tr2/game/objects/vehicles/boat.c index d3762c7fb..7464e5ceb 100644 --- a/src/tr2/game/objects/vehicles/boat.c +++ b/src/tr2/game/objects/vehicles/boat.c @@ -46,6 +46,7 @@ #define BOAT_TURN (DEG_1 / 8) // = 22 #define BOAT_MAX_TURN (DEG_1 * 4) // = 728 #define BOAT_SOUND_CEILING (WALL_L * 5) // = 5120 +#define BOAT_SHIFT_Y (-5) typedef enum { BOAT_STATE_GET_ON = 0, @@ -164,7 +165,7 @@ static int32_t M_TestWaterHeight( } } - return height - 5; + return height + BOAT_SHIFT_Y; } static void M_DoWakeEffect(const ITEM *const boat) @@ -618,7 +619,7 @@ static void M_Collision( ITEM *const boat = Item_Get(item_num); lara->pos.x = boat->pos.x; - lara->pos.y = boat->pos.y - 5; + lara->pos.y = boat->pos.y + BOAT_SHIFT_Y; lara->pos.z = boat->pos.z; lara->gravity = 0; lara->rot.x = 0; @@ -656,8 +657,8 @@ static void M_Control(const int16_t item_num) const int32_t hfr = M_TestWaterHeight(boat, BOAT_FRONT, BOAT_SIDE, &fr); int16_t room_num = boat->room_num; - const SECTOR *sector = - Room_GetSector(boat->pos.x, boat->pos.y, boat->pos.z, &room_num); + const SECTOR *sector = Room_GetSector( + boat->pos.x, boat->pos.y + BOAT_SHIFT_Y, boat->pos.z, &room_num); int32_t height = Room_GetHeight(sector, boat->pos.x, boat->pos.y, boat->pos.z); const int32_t ceiling = @@ -695,7 +696,7 @@ static void M_Control(const int16_t item_num) } } - boat->floor = height - 5; + boat->floor = height + BOAT_SHIFT_Y; if (boat_data->water == NO_HEIGHT) { boat_data->water = height; } else { @@ -741,7 +742,7 @@ static void M_Control(const int16_t item_num) Room_TestTriggers(boat); sector = Room_GetSector( - lara->pos.x, lara->pos.y - 5, lara->pos.z, &room_num); + lara->pos.x, lara->pos.y + BOAT_SHIFT_Y, lara->pos.z, &room_num); if (room_num != g_LaraItem->room_num) { Item_NewRoom(g_Lara.item_num, room_num); } @@ -769,7 +770,7 @@ static void M_Control(const int16_t item_num) : boat->speed; boat_data->pitch += ((pitch - boat_data->pitch) >> 2); - if (boat->speed != 0 && water_height - 5 != boat->pos.y) { + if (boat->speed != 0 && water_height + BOAT_SHIFT_Y != boat->pos.y) { Sound_Effect(SFX_BOAT_ENGINE, &boat->pos, SPM_NORMAL); } else if (boat->speed > 20) { Sound_Effect( @@ -784,7 +785,7 @@ static void M_Control(const int16_t item_num) + ((0x10000 - (BOAT_MAX_SPEED - boat_data->pitch) * 100) << 8)); } - if (boat->speed && water_height - 5 == boat->pos.y) { + if (boat->speed && water_height + BOAT_SHIFT_Y == boat->pos.y) { M_DoWakeEffect(boat); } diff --git a/src/tr2/game/objects/vehicles/skidoo_armed.c b/src/tr2/game/objects/vehicles/skidoo_armed.c index b3f60109c..01f88c96b 100644 --- a/src/tr2/game/objects/vehicles/skidoo_armed.c +++ b/src/tr2/game/objects/vehicles/skidoo_armed.c @@ -1,10 +1,10 @@ #include "game/items.h" #include "game/lara/control.h" -#include "game/lara/misc.h" #include "game/objects/creatures/skidoo_driver.h" #include "global/vars.h" #include +#include #include #define SKIDOO_ARMED_RADIUS (WALL_L / 3) // = 341 @@ -46,8 +46,7 @@ static void M_Collision( if (coll->enable_baddie_push) { Lara_Push( - item, lara_item, coll, item->speed > 0 ? coll->enable_hit : false, - false); + item, coll, item->speed > 0 ? coll->enable_hit : false, false); } if (g_Lara.skidoo == NO_ITEM && item->speed > 0) { diff --git a/src/tr2/game/option/option.c b/src/tr2/game/option/option.c index 6f1237358..0df640c96 100644 --- a/src/tr2/game/option/option.c +++ b/src/tr2/game/option/option.c @@ -73,7 +73,7 @@ void Option_Draw(INVENTORY_ITEM *const item) Option_Passport_Draw(item); break; case O_COMPASS_OPTION: - Option_Compass_Draw(item); + Option_Compass_Draw(); break; case O_DETAIL_OPTION: Option_Detail_Draw(item); diff --git a/src/tr2/game/option/option.h b/src/tr2/game/option/option.h index c6f81514d..5c6fd04fc 100644 --- a/src/tr2/game/option/option.h +++ b/src/tr2/game/option/option.h @@ -27,5 +27,5 @@ void Option_Controls_ShowControls(void); void Option_Controls_UpdateText(void); void Option_Compass_Control(INVENTORY_ITEM *item, bool is_busy); -void Option_Compass_Draw(INVENTORY_ITEM *item); +void Option_Compass_Draw(void); void Option_Compass_Shutdown(void); diff --git a/src/tr2/game/option/option_compass.c b/src/tr2/game/option/option_compass.c index 77ada6d52..ed4d36437 100644 --- a/src/tr2/game/option/option_compass.c +++ b/src/tr2/game/option/option_compass.c @@ -1,67 +1,76 @@ #include "game/game.h" #include "game/input.h" #include "game/option/option.h" -#include "game/requester.h" +#include "game/savegame.h" #include "game/sound.h" -#include "game/stats.h" -#include "game/ui/widgets/stats_dialog.h" #include "global/vars.h" +#include + #include -static UI_WIDGET *m_Dialog = nullptr; +typedef struct { + bool ui_active; + UI_STATS_DIALOG_STATE ui_state; +} M_PRIV; -static void M_Init(void); +static M_PRIV m_Priv = {}; -static void M_Init(void) +static void M_Init(M_PRIV *p); +static void M_Shutdown(M_PRIV *p); + +static void M_Init(M_PRIV *const p) { - m_Dialog = UI_StatsDialog_Create((UI_STATS_DIALOG_ARGS) { - .mode = Game_IsInGym() ? UI_STATS_DIALOG_MODE_ASSAULT_COURSE - : UI_STATS_DIALOG_MODE_LEVEL, - .level_num = Game_GetCurrentLevel()->num, - .style = UI_STATS_DIALOG_STYLE_BORDERED, - }); + p->ui_active = true; + UI_StatsDialog_Init( + &p->ui_state, + (UI_STATS_DIALOG_ARGS) { + .mode = Game_IsInGym() ? UI_STATS_DIALOG_MODE_ASSAULT_COURSE + : UI_STATS_DIALOG_MODE_LEVEL, + .level_num = Game_GetCurrentLevel()->num, + .style = UI_STATS_DIALOG_STYLE_BORDERED, + }); } -static void M_Shutdown(void) +static void M_Shutdown(M_PRIV *const p) { - if (m_Dialog != nullptr) { - m_Dialog->free(m_Dialog); - m_Dialog = nullptr; + if (p->ui_active) { + p->ui_active = false; + UI_StatsDialog_Free(&p->ui_state); } } -void Option_Compass_Control(INVENTORY_ITEM *const item, const bool is_busy) +void Option_Compass_Control(INVENTORY_ITEM *const inv_item, const bool is_busy) { + M_PRIV *const p = &m_Priv; if (is_busy) { return; } - char buffer[32]; - const int32_t sec = g_SaveGame.current_stats.timer / FRAMES_PER_SECOND; - sprintf(buffer, "%02d:%02d:%02d", sec / 3600, sec / 60 % 60, sec % 60); - - if (m_Dialog == nullptr) { - M_Init(); + if (!p->ui_active) { + M_Init(p); } - m_Dialog->control(m_Dialog); + UI_StatsDialog_Control(&p->ui_state); if (g_InputDB.menu_confirm || g_InputDB.menu_back) { - item->anim_direction = 1; - item->goal_frame = item->frames_total - 1; + M_Shutdown(p); + inv_item->anim_direction = 1; + inv_item->goal_frame = inv_item->frames_total - 1; } Sound_Effect(SFX_MENU_STOPWATCH, 0, SPM_ALWAYS); } -void Option_Compass_Draw(INVENTORY_ITEM *const item) +void Option_Compass_Draw(void) { - if (m_Dialog != nullptr) { - m_Dialog->draw(m_Dialog); + M_PRIV *const p = &m_Priv; + if (p->ui_active) { + UI_StatsDialog(&p->ui_state); } } void Option_Compass_Shutdown(void) { - M_Shutdown(); + M_PRIV *const p = &m_Priv; + M_Shutdown(p); } diff --git a/src/tr2/game/option/option_controls.c b/src/tr2/game/option/option_controls.c index b544d4cb0..d1fcc0392 100644 --- a/src/tr2/game/option/option_controls.c +++ b/src/tr2/game/option/option_controls.c @@ -1,61 +1,57 @@ #include "game/option/option.h" -#include "game/ui/widgets/controls_dialog.h" #include "global/vars.h" #include -#include +#include -static UI_WIDGET *m_Dialog; -static UI_CONTROLS_CONTROLLER m_Controller; -static int32_t m_Listener1; -static int32_t m_Listener2; +typedef struct { + int32_t listeners[2]; + struct { + bool is_ready; + UI_CONTROLS_STATE state; + } ui; +} M_PRIV; -static void M_Init(void); -static void M_Shutdown(void); +static M_PRIV m_Priv = {}; + +static void M_Init(M_PRIV *p); +static void M_Shutdown(M_PRIV *p); static void M_HandleLayoutChange(const EVENT *event, void *user_data); static void M_HandleKeyChange(const EVENT *event, void *user_data); -static void M_Init(void) +static void M_Init(M_PRIV *const p) { - UI_ControlsController_Init(&m_Controller); - m_Controller.active_layout = g_Config.input.keyboard_layout; - - m_Dialog = UI_ControlsDialog_Create(&m_Controller); - m_Listener1 = EventManager_Subscribe( - m_Controller.events, "layout_change", nullptr, M_HandleLayoutChange, - nullptr); - m_Listener2 = EventManager_Subscribe( - m_Controller.events, "key_change", nullptr, M_HandleKeyChange, nullptr); + UI_Controls_Init(&p->ui.state); + p->ui.is_ready = true; + p->listeners[0] = EventManager_Subscribe( + p->ui.state.events, "layout_change", nullptr, M_HandleLayoutChange, p); + p->listeners[1] = EventManager_Subscribe( + p->ui.state.events, "key_change", nullptr, M_HandleKeyChange, p); } -static void M_Shutdown(void) +static void M_Shutdown(M_PRIV *const p) { - if (m_Dialog == nullptr) { - return; + if (p->ui.is_ready) { + EventManager_Unsubscribe(p->ui.state.events, p->listeners[0]); + EventManager_Unsubscribe(p->ui.state.events, p->listeners[1]); + UI_Controls_Free(&p->ui.state); + p->ui.is_ready = false; } - - m_Dialog->free(m_Dialog); - m_Dialog = nullptr; - - EventManager_Unsubscribe(m_Controller.events, m_Listener1); - EventManager_Unsubscribe(m_Controller.events, m_Listener2); - - UI_ControlsController_Shutdown(&m_Controller); } static void M_HandleLayoutChange(const EVENT *event, void *user_data) { - switch (m_Controller.backend) { + const M_PRIV *const p = user_data; + switch (p->ui.state.backend) { case INPUT_BACKEND_KEYBOARD: - g_Config.input.keyboard_layout = m_Controller.active_layout; + g_Config.input.keyboard_layout = p->ui.state.active_layout; break; case INPUT_BACKEND_CONTROLLER: - g_Config.input.controller_layout = m_Controller.active_layout; + g_Config.input.controller_layout = p->ui.state.active_layout; break; default: break; } - Config_Write(); } @@ -66,22 +62,22 @@ static void M_HandleKeyChange(const EVENT *event, void *user_data) void Option_Controls_Shutdown(void) { - M_Shutdown(); + M_Shutdown(&m_Priv); } void Option_Controls_Control(INVENTORY_ITEM *const item, const bool is_busy) { + M_PRIV *const p = &m_Priv; if (is_busy) { return; } - if (m_Dialog == nullptr) { - M_Init(); + if (!p->ui.is_ready) { + M_Init(p); } - m_Dialog->control(m_Dialog); - if (m_Controller.state == UI_CONTROLS_STATE_EXIT) { - Option_Controls_Shutdown(); + if (UI_Controls_Control(&p->ui.state)) { + M_Shutdown(p); } else { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; @@ -90,7 +86,8 @@ void Option_Controls_Control(INVENTORY_ITEM *const item, const bool is_busy) void Option_Controls_Draw(INVENTORY_ITEM *const item) { - if (m_Dialog != nullptr) { - m_Dialog->draw(m_Dialog); + M_PRIV *const p = &m_Priv; + if (p->ui.is_ready) { + UI_Controls(&p->ui.state); } } diff --git a/src/tr2/game/option/option_detail.c b/src/tr2/game/option/option_detail.c index 9ac45a404..2f4fb0184 100644 --- a/src/tr2/game/option/option_detail.c +++ b/src/tr2/game/option/option_detail.c @@ -1,16 +1,57 @@ #include "game/input.h" #include "game/option/option.h" #include "game/text.h" +#include "game/ui/dialogs/graphic_settings.h" #include "global/vars.h" +typedef struct { + struct { + bool is_ready; + UI_GRAPHIC_SETTINGS_STATE state; + } ui; +} M_PRIV; + +static M_PRIV m_Priv = {}; + +static void M_Init(M_PRIV *p); +static void M_Shutdown(M_PRIV *p); + +static void M_Init(M_PRIV *const p) +{ + p->ui.is_ready = true; + UI_GraphicSettings_Init(&p->ui.state); +} + +static void M_Shutdown(M_PRIV *const p) +{ + if (p->ui.is_ready) { + p->ui.is_ready = false; + UI_GraphicSettings_Free(&p->ui.state); + } +} + void Option_Detail_Control(INVENTORY_ITEM *const item, const bool is_busy) { + M_PRIV *const p = &m_Priv; + if (is_busy) { + return; + } + if (!p->ui.is_ready) { + M_Init(p); + } + UI_GraphicSettings_Control(&p->ui.state); } void Option_Detail_Draw(INVENTORY_ITEM *const item) { + M_PRIV *const p = &m_Priv; + if (p->ui.is_ready) { + UI_GraphicSettings(&p->ui.state); + } } void Option_Detail_Shutdown(void) { + M_PRIV *const p = &m_Priv; + M_Shutdown(p); } diff --git a/src/tr2/game/option/option_passport.c b/src/tr2/game/option/option_passport.c index 6c90706a2..1c56e5f78 100644 --- a/src/tr2/game/option/option_passport.c +++ b/src/tr2/game/option/option_passport.c @@ -1,18 +1,19 @@ #include "decomp/decomp.h" -#include "decomp/savegame.h" #include "game/game.h" #include "game/game_flow.h" #include "game/game_string.h" #include "game/input.h" +#include "game/inventory.h" #include "game/inventory_ring.h" #include "game/option/option.h" -#include "game/requester.h" +#include "game/savegame.h" #include "game/sound.h" #include "game/text.h" #include "global/vars.h" #include #include +#include typedef enum { M_ROLE_LOAD_GAME, @@ -22,30 +23,64 @@ typedef enum { M_ROLE_EXIT, } M_PAGE_ROLE; +typedef enum { + M_MODE_BROWSE, + M_MODE_PICK_OPTION, +} M_PAGE_MODE; + typedef struct { bool available; M_PAGE_ROLE role; } M_PAGE; static struct { + M_PAGE_MODE mode; int32_t current_page; int32_t active_page; int32_t selection; M_PAGE pages[3]; - bool page_ready; + bool is_ready; + struct { + bool is_ready; + UI_NEW_GAME_STATE state; + } new_game; + struct { + UI_PLAY_ANY_LEVEL_DIALOG_STATE *state; + } play_any_level; + struct { + UI_SAVE_SLOT_DIALOG_STATE *state; + } save_slot; } m_State = { .active_page = -1 }; TEXTSTRING *m_SubtitleText = nullptr; +static void M_ChangePageTextContent(const char *title); static void M_SetPage(int32_t page, M_PAGE_ROLE role, bool available); +static void M_InitRequesters(void); +static void M_FreeRequesters(void); static void M_DeterminePages(void); static void M_RemoveAllText(void); +static void M_InitSaveRequester(M_PAGE_ROLE page_role); +static void M_ShowSaves(INVENTORY_ITEM *inv_item); +static void M_LoadGame(INVENTORY_ITEM *inv_item); +static void M_SaveGame(INVENTORY_ITEM *inv_item); +static void M_NewGame(void); +static void M_PlayAnyLevel(INVENTORY_ITEM *inv_item); static void M_FlipLeft(INVENTORY_ITEM *inv_item); static void M_FlipRight(INVENTORY_ITEM *inv_item); static void M_Close(INVENTORY_ITEM *inv_item); -static void M_ShowPage(const INVENTORY_ITEM *inv_item); +static void M_ShowPage(INVENTORY_ITEM *inv_item); static void M_HandleFlipInputs(void); +static void M_ChangePageTextContent(const char *const title) +{ + if (m_SubtitleText == nullptr) { + m_SubtitleText = Text_Create(0, -16, title); + Text_AlignBottom(m_SubtitleText, true); + Text_CentreH(m_SubtitleText, true); + } +} + static void M_SetPage( const int32_t page, const M_PAGE_ROLE role, const bool available) { @@ -53,9 +88,21 @@ static void M_SetPage( m_State.pages[page].available = available; } +static void M_InitRequesters(void) +{ + UI_NewGame_Init(&m_State.new_game.state); +} + +static void M_FreeRequesters(void) +{ + UI_NewGame_Free(&m_State.new_game.state); + m_State.new_game.is_ready = false; + m_State.is_ready = false; +} + static void M_DeterminePages(void) { - const bool has_saves = g_SavedGames != 0; + const bool has_saves = Savegame_GetTotalCount() != 0; for (int32_t i = 0; i < 3; i++) { m_State.pages[i].available = false; @@ -63,6 +110,7 @@ static void M_DeterminePages(void) switch (g_Inv_Mode) { case INV_TITLE_MODE: + m_State.mode = M_MODE_BROWSE; M_SetPage(0, M_ROLE_LOAD_GAME, has_saves); M_SetPage( 1, @@ -72,17 +120,20 @@ static void M_DeterminePages(void) break; case INV_GAME_MODE: + m_State.mode = M_MODE_BROWSE; M_SetPage(0, M_ROLE_LOAD_GAME, has_saves); M_SetPage(1, M_ROLE_SAVE_GAME, true); M_SetPage(2, M_ROLE_EXIT, true); break; case INV_DEATH_MODE: + m_State.mode = M_MODE_BROWSE; M_SetPage(0, M_ROLE_LOAD_GAME, has_saves); M_SetPage(2, M_ROLE_EXIT, true); break; case INV_LOAD_MODE: + m_State.mode = M_MODE_BROWSE; if (has_saves) { M_SetPage(0, M_ROLE_LOAD_GAME, true); } else { @@ -91,6 +142,7 @@ static void M_DeterminePages(void) break; case INV_SAVE_MODE: + m_State.mode = M_MODE_BROWSE; M_SetPage(1, M_ROLE_SAVE_GAME, true); break; @@ -135,16 +187,135 @@ static void M_DeterminePages(void) static void M_RemoveAllText(void) { - if (g_LoadGameRequester.ready) { - Requester_Shutdown(&g_LoadGameRequester); - } - if (g_SaveGameRequester.ready) { - Requester_Shutdown(&g_SaveGameRequester); - } if (m_SubtitleText != nullptr) { Text_Remove(m_SubtitleText); m_SubtitleText = nullptr; } + if (m_State.save_slot.state != nullptr) { + UI_SaveSlotDialog_Free(m_State.save_slot.state); + m_State.save_slot.state = nullptr; + } + if (m_State.play_any_level.state != nullptr) { + UI_SaveSlotDialog_Free(m_State.save_slot.state); + m_State.play_any_level.state = nullptr; + } + M_FreeRequesters(); +} + +static void M_InitSaveRequester(const M_PAGE_ROLE role) +{ + int32_t save_slot = Savegame_GetMostRecentlyUsedSlot(); + if (save_slot == -1) { + save_slot = Savegame_GetMostRecentlyCreatedSlot(); + } + + const UI_SAVE_SLOT_DIALOG_TYPE dialog_type = role == M_ROLE_LOAD_GAME + ? UI_SAVE_SLOT_DIALOG_LOAD_GAME + : UI_SAVE_SLOT_DIALOG_SAVE_GAME; + m_State.save_slot.state = UI_SaveSlotDialog_Init(dialog_type, save_slot); +} + +static void M_ShowSaves(INVENTORY_ITEM *const inv_item) +{ + if (m_State.save_slot.state == nullptr) { + M_InitSaveRequester(m_State.pages[m_State.active_page].role); + } + const UI_SAVE_SLOT_DIALOG_CHOICE choice = + UI_SaveSlotDialog_Control(m_State.save_slot.state); + switch (choice.action) { + case UI_SAVE_SLOT_DIALOG_NO_CHOICE: + // prevent propagating confirmation input up + if (g_Input.menu_confirm) { + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + } + break; + + case UI_SAVE_SLOT_DIALOG_CANCEL: + if (g_Inv_Mode != INV_SAVE_MODE && g_Inv_Mode != INV_LOAD_MODE) { + M_Close(inv_item); + } + break; + + case UI_SAVE_SLOT_DIALOG_DETAILS: + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + break; + + case UI_SAVE_SLOT_DIALOG_CONFIRM: + m_State.selection = choice.slot_num; + break; + } +} + +static void M_LoadGame(INVENTORY_ITEM *const inv_item) +{ + M_ChangePageTextContent(GS(PASSPORT_LOAD_GAME)); + M_ShowSaves(inv_item); +} + +static void M_SaveGame(INVENTORY_ITEM *const inv_item) +{ + M_ChangePageTextContent(GS(PASSPORT_SAVE_GAME)); + M_ShowSaves(inv_item); +} + +static void M_NewGame(void) +{ + M_ChangePageTextContent(GS(PASSPORT_NEW_GAME)); + m_State.selection = GF_GetFirstLevel()->num; + if (m_State.mode == M_MODE_BROWSE) { + if (g_InputDB.menu_confirm + && (g_Config.gameplay.enable_game_modes + || g_Config.profile.new_game_plus_unlock)) { + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + m_State.mode = M_MODE_PICK_OPTION; + m_State.new_game.is_ready = true; + } + } else if (m_State.mode == M_MODE_PICK_OPTION) { + const int32_t choice = UI_NewGame_Control(&m_State.new_game.state); + if (choice == UI_REQUESTER_NO_CHOICE) { + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + } else if (choice == UI_REQUESTER_CANCEL) { + m_State.mode = M_MODE_BROWSE; + m_State.new_game.is_ready = false; + g_Input = (INPUT_STATE) {}; + g_InputDB = (INPUT_STATE) {}; + } else { + switch (choice) { + case 0: + Game_SetBonusFlag(GBF_NONE); + break; + case 1: + Game_SetBonusFlag(GBF_NGPLUS); + break; + case 2: + Game_SetBonusFlag(GBF_JAPANESE); + break; + case 3: + Game_SetBonusFlag(GBF_JAPANESE | GBF_NGPLUS); + break; + default: + Game_SetBonusFlag(GBF_NONE); + break; + } + } + } +} + +static void M_PlayAnyLevel(INVENTORY_ITEM *const inv_item) +{ + M_ChangePageTextContent(GS(PASSPORT_NEW_GAME)); + if (m_State.play_any_level.state == nullptr) { + m_State.play_any_level.state = UI_PlayAnyLevelDialog_Init(); + } + const int32_t choice = + UI_PlayAnyLevelDialog_Control(m_State.play_any_level.state); + if (choice != UI_PLAY_ANY_LEVEL_CHOICE_NO_CHOICE) { + m_State.selection = choice; + } } static void M_FlipLeft(INVENTORY_ITEM *const inv_item) @@ -165,6 +336,7 @@ static void M_FlipRight(INVENTORY_ITEM *const inv_item) static void M_Close(INVENTORY_ITEM *const inv_item) { + m_State.active_page = -1; M_RemoveAllText(); if (m_State.current_page == 2) { inv_item->anim_direction = 1; @@ -175,90 +347,32 @@ static void M_Close(INVENTORY_ITEM *const inv_item) } } -static void M_ShowPage(const INVENTORY_ITEM *const inv_item) +static void M_ShowPage(INVENTORY_ITEM *const inv_item) { switch (m_State.pages[m_State.active_page].role) { case M_ROLE_LOAD_GAME: - if (m_SubtitleText == nullptr) { - m_SubtitleText = Text_Create(0, -16, GS(PASSPORT_LOAD_GAME)); - Text_AlignBottom(m_SubtitleText, true); - Text_CentreH(m_SubtitleText, true); - } - - if (!g_LoadGameRequester.ready) { - GetSavedGamesList(&g_LoadGameRequester); - Requester_SetHeading( - &g_LoadGameRequester, GS(PASSPORT_LOAD_GAME), 0, nullptr, 0); - Requester_SetSize(&g_LoadGameRequester, 10, -32); - g_LoadGameRequester.ready = true; - } - m_State.selection = - Requester_Display(&g_LoadGameRequester, false, true) - 1; - if (m_State.selection >= 0 && !g_SavedLevels[m_State.selection]) { - m_State.selection = -1; - g_Input = (INPUT_STATE) {}; - g_InputDB = (INPUT_STATE) {}; - } + M_LoadGame(inv_item); break; - case M_ROLE_SAVE_GAME: { - if (m_SubtitleText == nullptr) { - m_SubtitleText = Text_Create(0, -16, GS(PASSPORT_SAVE_GAME)); - Text_AlignBottom(m_SubtitleText, true); - Text_CentreH(m_SubtitleText, true); - } - - if (!g_LoadGameRequester.ready) { - Requester_SetHeading( - &g_LoadGameRequester, GS(PASSPORT_SAVE_GAME), 0, nullptr, 0); - Requester_SetSize(&g_LoadGameRequester, 10, -32); - g_LoadGameRequester.ready = true; - } - m_State.selection = - Requester_Display(&g_LoadGameRequester, true, true) - 1; + case M_ROLE_SAVE_GAME: + M_SaveGame(inv_item); break; - } - - case M_ROLE_PLAY_ANY_LEVEL: { - if (!g_SaveGameRequester.ready) { - Requester_Init(&g_SaveGameRequester); - Requester_SetSize(&g_SaveGameRequester, 10, -32); - GetValidLevelsList(&g_SaveGameRequester); - Requester_SetHeading( - &g_SaveGameRequester, GS(PASSPORT_SELECT_LEVEL), 0, nullptr, 0); - g_SaveGameRequester.ready = true; - } - if (m_SubtitleText == nullptr) { - m_SubtitleText = Text_Create(0, -16, GS(PASSPORT_NEW_GAME)); - Text_AlignBottom(m_SubtitleText, true); - Text_CentreH(m_SubtitleText, true); - } - m_State.selection = - Requester_Display(&g_SaveGameRequester, true, true) - 1; - break; - } case M_ROLE_NEW_GAME: - if (m_SubtitleText == nullptr) { - m_SubtitleText = Text_Create(0, -16, GS(PASSPORT_NEW_GAME)); - Text_AlignBottom(m_SubtitleText, true); - Text_CentreH(m_SubtitleText, true); - } - m_State.selection = GF_GetFirstLevel()->num; + M_NewGame(); + break; + + case M_ROLE_PLAY_ANY_LEVEL: + M_PlayAnyLevel(inv_item); break; case M_ROLE_EXIT: - if (m_SubtitleText == nullptr) { - if (g_Inv_Mode == INV_TITLE_MODE) { - m_SubtitleText = Text_Create(0, -16, GS(PASSPORT_EXIT_GAME)); - } else if (g_GameFlow.is_demo_version) { - m_SubtitleText = Text_Create(0, -16, GS(PASSPORT_EXIT_DEMO)); - } else { - m_SubtitleText = - Text_Create(0, -16, GS(PASSPORT_EXIT_TO_TITLE)); - } - Text_AlignBottom(m_SubtitleText, true); - Text_CentreH(m_SubtitleText, true); + if (g_Inv_Mode == INV_TITLE_MODE) { + M_ChangePageTextContent(GS(PASSPORT_EXIT_GAME)); + } else if (g_GameFlow.is_demo_version) { + M_ChangePageTextContent(GS(PASSPORT_EXIT_DEMO)); + } else { + M_ChangePageTextContent(GS(PASSPORT_EXIT_TO_TITLE)); } break; } @@ -286,6 +400,7 @@ static void M_HandleFlipInputs(void) void Option_Passport_Control(INVENTORY_ITEM *const item, const bool is_busy) { if (m_State.active_page == -1) { + M_InitRequesters(); M_DeterminePages(); } @@ -311,19 +426,18 @@ void Option_Passport_Control(INVENTORY_ITEM *const item, const bool is_busy) } else if (m_State.current_page > m_State.active_page) { M_FlipLeft(item); } else { + m_State.is_ready = true; M_ShowPage(item); if (g_InputDB.menu_confirm) { g_Inv_ExtraData[0] = m_State.active_page; g_Inv_ExtraData[1] = m_State.selection; - m_State.active_page = -1; M_Close(item); } else if (g_InputDB.menu_back) { - if (g_Inv_Mode != INV_DEATH_MODE) { - m_State.active_page = -1; - M_Close(item); - } else { + if (g_Inv_Mode == INV_DEATH_MODE) { g_Input = (INPUT_STATE) {}; g_InputDB = (INPUT_STATE) {}; + } else { + M_Close(item); } } else { M_HandleFlipInputs(); @@ -333,10 +447,34 @@ void Option_Passport_Control(INVENTORY_ITEM *const item, const bool is_busy) void Option_Passport_Draw(INVENTORY_ITEM *const item) { + switch (m_State.pages[m_State.active_page].role) { + case M_ROLE_NEW_GAME: + if (m_State.new_game.is_ready) { + UI_NewGame(&m_State.new_game.state); + } + break; + + case M_ROLE_PLAY_ANY_LEVEL: + if (m_State.play_any_level.state != nullptr) { + UI_PlayAnyLevelDialog(m_State.play_any_level.state); + } + break; + + case M_ROLE_LOAD_GAME: + case M_ROLE_SAVE_GAME: + if (m_State.is_ready && m_State.save_slot.state != nullptr) { + UI_SaveSlotDialog(m_State.save_slot.state); + } + break; + + default: + break; + } } void Option_Passport_Shutdown(void) { M_RemoveAllText(); + UI_NewGame_Free(&m_State.new_game.state); m_State.active_page = -1; } diff --git a/src/tr2/game/output.c b/src/tr2/game/output.c index 5d4d3f3ee..4f4f586d8 100644 --- a/src/tr2/game/output.c +++ b/src/tr2/game/output.c @@ -2,18 +2,23 @@ #include "game/clock.h" #include "game/inventory_ring.h" +#include "game/level.h" #include "game/random.h" #include "game/render/common.h" #include "game/render/priv.h" -#include "game/scaler.h" #include "game/shell.h" +#include "game/viewport.h" #include "global/vars.h" +#include #include #include #include #include +#include #include +#include +#include #include typedef enum { @@ -52,6 +57,7 @@ static NAMED_COLOR m_NamedColors[COLOR_NUMBER_OF] = { // clang-format on }; +static int32_t m_VBufCapacity = 0; static int32_t m_TickComp = 0; static int32_t m_LsAdder = 0; static int32_t m_LsDivider = 0; @@ -62,13 +68,20 @@ static int16_t m_ShadesTable[32]; static int32_t m_RandomTable[32]; static BACKGROUND_TYPE m_BackgroundType = BK_TRANSPARENT; static XYZ_32 m_LsVectorView = {}; + +static int32_t m_FogEnd = 0; +static RGB_F m_WaterColor = {}; + static bool m_IsWaterEffect = false; static bool m_IsWibbleEffect = false; static bool m_IsShadeEffect = false; static int32_t m_WibbleOffset = 0; + static bool m_IsSunsetEnabled = false; static int32_t m_SunsetTimer = 0; +static int32_t m_DepthBias = 0; + static void M_CalcRoomVertices(const ROOM_MESH *mesh, int32_t far_clip); static void M_CalcRoomVerticesWibble(const ROOM_MESH *mesh); static void M_DrawRoomSprites(const ROOM_MESH *mesh); @@ -171,19 +184,14 @@ static void M_CalcRoomVertices(const ROOM_MESH *const mesh, int32_t far_clip) const double persp = g_FltPersp / zv; const int32_t depth = zv_int >> W2V_SHIFT; vbuf->zv += base_z; - - if (depth < FOG_END) { - if (depth > FOG_START) { - shade += depth - FOG_START; - } - vbuf->rhw = persp * g_FltRhwOPersp; - } else { - // clip_flags = far_clip; - shade = 0x1FFF; + if (zv >= Output_GetFarZ()) { vbuf->zv = g_FltFarZ; - vbuf->rhw = persp * g_FltRhwOPersp; } + shade += Output_CalcFogShade(depth); + + vbuf->rhw = persp * g_FltRhwOPersp; + double xs = xv * persp + g_FltWinCenterX; double ys = yv * persp + g_FltWinCenterY; @@ -204,7 +212,7 @@ static void M_CalcRoomVertices(const ROOM_MESH *const mesh, int32_t far_clip) // clip_flags |= (~((uint8_t)(vbuf->zv / 0x155555.p0))) << 8; } - CLAMP(shade, 0, 0x1FFF); + CLAMP(shade, 0, SHADE_MAX); vbuf->g = shade; vbuf->clip = clip_flags; } @@ -295,9 +303,11 @@ static bool M_CalcObjectVertices( } const double persp = g_FltPersp / zv; + const double persp_biased = + g_FltPersp / (zv + (m_DepthBias << W2V_SHIFT)); vbuf->xs = persp * xv + g_FltWinCenterX; vbuf->ys = persp * yv + g_FltWinCenterY; - vbuf->rhw = persp * g_FltRhwOPersp; + vbuf->rhw = persp_biased * g_FltRhwOPersp; clip_flags = 0x00; if (vbuf->xs < g_FltWinLeft) { @@ -354,7 +364,7 @@ static void M_CalcVerticeLight(const OBJECT_MESH *const mesh) if (mesh->num_lights <= 0) { for (int32_t i = 0; i < -mesh->num_lights; i++) { int16_t shade = m_LsAdder + mesh->lighting.lights[i]; - CLAMP(shade, 0, 0x1FFF); + CLAMP(shade, 0, SHADE_MAX); g_PhdVBuf[i].g = shade; } @@ -363,7 +373,7 @@ static void M_CalcVerticeLight(const OBJECT_MESH *const mesh) if (m_LsDivider == 0) { int16_t shade = m_LsAdder; - CLAMP(shade, 0, 0x1FFF); + CLAMP(shade, 0, SHADE_MAX); for (int32_t i = 0; i < mesh->num_lights; i++) { g_PhdVBuf[i].g = shade; } @@ -396,7 +406,7 @@ static void M_CalcVerticeLight(const OBJECT_MESH *const mesh) const XYZ_16 *const normal = &mesh->lighting.normals[i]; int16_t shade = m_LsAdder + ((xv * normal->x + yv * normal->y + zv * normal->z) >> 16); - CLAMP(shade, 0, 0x1FFF); + CLAMP(shade, 0, SHADE_MAX); g_PhdVBuf[i].g = shade; } } @@ -408,6 +418,75 @@ static void M_CalcSkyboxLight(const OBJECT_MESH *const mesh) } } +static void M_ReserveVertexBuffer(void) +{ + BENCHMARK benchmark = Benchmark_Start(); + int32_t max_vertices = 1500; + for (int32_t i = 0; i < O_NUMBER_OF; i++) { + const OBJECT *const obj = Object_Get(i); + if (!obj->loaded) { + continue; + } + + for (int32_t j = 0; j < obj->mesh_count; j++) { + const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx + j); + max_vertices = MAX(max_vertices, mesh->num_vertices); + } + } + + for (int32_t i = 0; i < MAX_STATIC_OBJECTS_3D; i++) { + const STATIC_OBJECT_3D *obj = Object_Get3DStatic(i); + if (!obj->loaded) { + continue; + } + + const OBJECT_MESH *const mesh = Object_GetMesh(obj->mesh_idx); + max_vertices = MAX(max_vertices, mesh->num_vertices); + } + + for (int32_t i = 0; i < Room_GetCount(); i++) { + const ROOM *const room = Room_Get(i); + max_vertices = MAX(max_vertices, room->mesh.num_vertices); + } + + Benchmark_End(&benchmark, nullptr); + if (max_vertices >= m_VBufCapacity) { + g_PhdVBuf = Memory_Realloc(g_PhdVBuf, max_vertices * sizeof(PHD_VBUF)); + m_VBufCapacity = max_vertices; + } +} + +void Output_ApplyLevelSettings(void) +{ + Output_SetWaterColor(Level_GetWaterColor()); + Output_SetFogStart(Level_GetFogStart() * WALL_L); + Output_SetFogEnd(Level_GetFogEnd() * WALL_L); + Viewport_Reset(); +} + +void Output_ObserveLevelLoad(void) +{ + M_ReserveVertexBuffer(); + Output_ApplyLevelSettings(); + for (int32_t i = 0; i < COLOR_NUMBER_OF; i++) { + m_NamedColors[i].palette_index = + Output_FindColor8(m_NamedColors[i].rgb); + } +} + +void Output_ObserveLevelUnload(void) +{ + Output_InitialiseTexturePages(0, true); + Output_InitialiseObjectTextures(0); + if (Output_GetBackgroundType() == BK_OBJECT) { + Output_UnloadBackground(); + } +} + +void Output_ObserveRoomFlip(const ROOM *room) +{ +} + void Output_DrawObjectMesh(const OBJECT_MESH *const mesh, const int32_t clip) { g_FltWinLeft = 0.0f; @@ -590,14 +669,14 @@ void Output_DrawSprite( if (flags & SPRF_SHADE) { const int32_t depth = zv >> W2V_SHIFT; - if (depth > FOG_START) { - shade += depth - FOG_START; - if (shade > 0x1FFF) { + if (depth > Output_GetFogStart()) { + shade += depth - Output_GetFogStart(); + if (shade > SHADE_MAX) { return; } } } else { - shade = 0x1000; + shade = SHADE_NEUTRAL; } Render_InsertSprite(zv, x0, y0, x1, y1, sprite_idx, shade); @@ -660,14 +739,9 @@ BACKGROUND_TYPE Output_GetBackgroundType(void) return m_BackgroundType; } -bool Output_LoadBackgroundFromFile(const char *const file_name) +bool Output_LoadBackgroundFromImage(const IMAGE *const image) { - IMAGE *const image = Image_CreateFromFile(file_name); - if (image == nullptr) { - return false; - } Render_LoadBackgroundFromImage(image); - Image_Free(image); m_BackgroundType = BK_IMAGE; return true; } @@ -696,6 +770,7 @@ void Output_UnloadBackground(void) { Render_UnloadBackground(); m_BackgroundType = BK_TRANSPARENT; + Output_ClearLastBackgroundPath(); } void Output_InsertBackPolygon( @@ -825,7 +900,7 @@ void Output_CalculateWibbleTable(void) for (int32_t i = 0; i < WIBBLE_SIZE; i++) { const int32_t sine = Math_Sin(i * DEG_360 / WIBBLE_SIZE); m_WibbleTable[i] = (sine * MAX_WIBBLE) >> W2V_SHIFT; - m_ShadesTable[i] = (sine * MAX_SHADE) >> W2V_SHIFT; + m_ShadesTable[i] = (sine * SHADE_CAUSTICS) >> W2V_SHIFT; m_RandomTable[i] = (Random_GetDraw() >> 5) - 0x01FF; for (int32_t j = 0; j < WIBBLE_SIZE; j++) { m_RoomLightTables[i].table[j] = (j - (WIBBLE_SIZE / 2)) * i @@ -900,6 +975,41 @@ int32_t Output_GetObjectBounds(const BOUNDS_16 *const bounds) return 1; } +int32_t Output_GetNearZ(void) +{ + return 20 << W2V_SHIFT; +} + +int32_t Output_GetFarZ(void) +{ + return Output_GetFogEnd() << W2V_SHIFT; +} + +void Output_SetWaterColor(const RGB_888 color) +{ + m_WaterColor.r = color.r / 255.0f; + m_WaterColor.g = color.g / 255.0f; + m_WaterColor.b = color.b / 255.0f; +} + +RGB_F Output_GetTint(void) +{ + if (Output_IsShadeEffect()) { + return m_WaterColor; + } + return (RGB_F) { 1.0f, 1.0f, 1.0f }; +} + +int32_t Output_GetFogEnd(void) +{ + return m_FogEnd; +} + +void Output_SetFogEnd(const int32_t dist) +{ + m_FogEnd = dist; +} + void Output_SetupBelowWater(const bool is_underwater) { Render_SetWet(is_underwater); @@ -974,13 +1084,15 @@ void Output_SetLightDivider(const int32_t divider) int32_t Output_CalcFogShade(const int32_t depth) { - if (depth > FOG_START) { - return depth - FOG_START; + const int32_t fog_start = Output_GetFogStart(); + const int32_t fog_end = Output_GetFogEnd(); + if (depth < fog_start) { + return 0; } - if (depth > FOG_END) { - return 0x1FFF; + if (depth >= fog_end) { + return SHADE_MAX; } - return 0; + return (depth - fog_start) * SHADE_MAX / (fog_end - fog_start); } int32_t Output_GetRoomLightShade(const ROOM_LIGHT_MODE mode) @@ -1000,10 +1112,56 @@ void Output_LightRoomVertices(const ROOM *const room) } } -void Output_InitialiseNamedColors(void) +void Output_DrawTextOutline( + const UI_STYLE ui_style, const int32_t x, const int32_t y, + const int32_t width, const int32_t height, const int32_t z, + const TEXT_STYLE text_style) { - for (int32_t i = 0; i < COLOR_NUMBER_OF; i++) { - m_NamedColors[i].palette_index = - Output_FindColor8(m_NamedColors[i].rgb); - } + const int32_t mesh_idx = Object_Get(O_TEXT_BOX)->mesh_idx; + + const int32_t offset = 4; + const int32_t x0 = x + offset; + const int32_t y0 = y + offset; + const int32_t x1 = x0 + width - offset * 2; + const int32_t y1 = y0 + height - offset * 2; + const int32_t scale_h = TEXT_BASE_SCALE; + const int32_t scale_v = TEXT_BASE_SCALE; + + Output_DrawScreenSprite( + x0, y0, z, scale_h, scale_v, mesh_idx + 0, SHADE_NEUTRAL, 0); + Output_DrawScreenSprite( + x1, y0, z, scale_h, scale_v, mesh_idx + 1, SHADE_NEUTRAL, 0); + Output_DrawScreenSprite( + x1, y1, z, scale_h, scale_v, mesh_idx + 2, SHADE_NEUTRAL, 0); + Output_DrawScreenSprite( + x0, y1, z, scale_h, scale_v, mesh_idx + 3, SHADE_NEUTRAL, 0); + + int32_t w = (width - offset * 2) * TEXT_BASE_SCALE / 8; + int32_t h = (height - offset * 2) * TEXT_BASE_SCALE / 8; + + Output_DrawScreenSprite( + x0, y0, z, w, scale_v, mesh_idx + 4, SHADE_NEUTRAL, 0); + Output_DrawScreenSprite( + x1, y0, z, scale_h, h, mesh_idx + 5, SHADE_NEUTRAL, 0); + Output_DrawScreenSprite( + x0, y1, z, w, scale_v, mesh_idx + 6, SHADE_NEUTRAL, 0); + Output_DrawScreenSprite( + x0, y0, z, scale_h, h, mesh_idx + 7, SHADE_NEUTRAL, 0); +} + +void Output_DrawTextBackground( + const UI_STYLE ui_style, const int32_t x, const int32_t y, const int32_t w, + const int32_t h, const int32_t z, const TEXT_STYLE text_style) +{ + Output_DrawScreenFBox(x, y, z, w, h, 0, nullptr, 0); +} + +int32_t Output_GetDepthBias(void) +{ + return m_DepthBias; +} + +void Output_SetDepthBias(const int32_t bias) +{ + m_DepthBias = bias; } diff --git a/src/tr2/game/output.h b/src/tr2/game/output.h index 703dc7402..b09802eed 100644 --- a/src/tr2/game/output.h +++ b/src/tr2/game/output.h @@ -22,6 +22,8 @@ typedef struct { float g; } VERTEX_INFO; +void Output_ApplyLevelSettings(void); + void Output_DrawObjectMesh(const OBJECT_MESH *mesh, int32_t clip); void Output_DrawObjectMesh_I(const OBJECT_MESH *mesh, int32_t clip); void Output_DrawRoom(const ROOM_MESH *mesh, bool is_outside); @@ -74,7 +76,6 @@ void Output_DrawAirBar(int32_t percent); BACKGROUND_TYPE Output_GetBackgroundType(void); void Output_LoadBackgroundFromObject(void); -void Output_InitialiseNamedColors(void); void Output_InsertShadow( int16_t radius, const BOUNDS_16 *bounds, const ITEM *item); @@ -87,3 +88,12 @@ void Output_SetShadeEffect(bool shade_effect); bool Output_IsShadeEffect(void); void Output_SetSunsetEnabled(bool enabled); void Output_SetSunsetTimer(int32_t timer); + +int32_t Output_GetNearZ(void); +int32_t Output_GetFarZ(void); + +void Output_SetWaterColor(RGB_888 color); +RGB_F Output_GetTint(void); + +int32_t Output_GetDepthBias(void); +void Output_SetDepthBias(int32_t bias); diff --git a/src/tr2/game/overlay.c b/src/tr2/game/overlay.c index 1ee19e016..84d697f77 100644 --- a/src/tr2/game/overlay.c +++ b/src/tr2/game/overlay.c @@ -4,19 +4,20 @@ #include "game/clock.h" #include "game/game.h" #include "game/game_flow.h" -#include "game/gym.h" #include "game/inventory.h" #include "game/music.h" #include "game/objects/common.h" #include "game/objects/vars.h" #include "game/output.h" -#include "game/scaler.h" +#include "game/savegame.h" #include "game/text.h" #include "game/viewport.h" #include "global/vars.h" #include +#include #include +#include #include #include @@ -162,9 +163,10 @@ static void M_DrawAssaultTimer(void) } char buffer[32]; - const int32_t total_sec = - g_SaveGame.current_stats.timer / FRAMES_PER_SECOND; - const int32_t frame = g_SaveGame.current_stats.timer % FRAMES_PER_SECOND; + const RESUME_INFO *const resume = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + const int32_t total_sec = resume->stats.timer / FRAMES_PER_SECOND; + const int32_t frame = resume->stats.timer % FRAMES_PER_SECOND; sprintf( buffer, "%d:%02d.%d", total_sec / 60, total_sec % 60, frame * 10 / FRAMES_PER_SECOND); @@ -209,7 +211,8 @@ static void M_DrawAssaultTimer(void) glyph_info[glyph_type].offset, SCALER_TARGET_ASSAULT_DIGITS); Output_DrawScreenSprite( x, y, 0, scale_h, scale_v, - Object_Get(O_ASSAULT_DIGITS)->mesh_idx + mesh_num, 0x1000, 0); + Object_Get(O_ASSAULT_DIGITS)->mesh_idx + mesh_num, SHADE_NEUTRAL, + 0); x += Scaler_Calc( glyph_info[glyph_type].width, SCALER_TARGET_ASSAULT_DIGITS); } @@ -257,7 +260,7 @@ static void M_DrawAirBar(void) static void M_DrawAmmoInfo(void) { if (g_Lara.gun_status != LGS_READY || g_OverlayStatus <= 0 - || g_SaveGame.bonus_flag) { + || Game_IsBonusFlagSet(GBF_NGPLUS)) { if (m_AmmoTextInfo != nullptr) { Text_Remove(m_AmmoTextInfo); m_AmmoTextInfo = nullptr; @@ -454,7 +457,7 @@ static void M_DrawPickup3D(const DISPLAY_PICKUP *const pickup) Matrix_RotY(pickup->rot_y); Output_SetLightDivider(0x6000); - Output_SetLightAdder(LOW_LIGHT); + Output_SetLightAdder(SHADE_LOW); Output_RotateLight(0, 0); Matrix_Push(); @@ -502,7 +505,7 @@ static void M_DrawPickupSprite(const DISPLAY_PICKUP *const pickup) // TODO: use proper scaling const int32_t scale = 12288 * g_PhdWinWidth / 640; const int16_t sprite_num = pickup->object->mesh_idx; - Output_DrawPickup(x, y, scale, sprite_num, 4096); + Output_DrawPickup(x, y, scale, sprite_num, SHADE_NEUTRAL); } static void M_DrawPickups(void) diff --git a/src/tr2/game/render/hwr.c b/src/tr2/game/render/hwr.c index 2f00339fd..adc99c2ae 100644 --- a/src/tr2/game/render/hwr.c +++ b/src/tr2/game/render/hwr.c @@ -29,7 +29,6 @@ typedef struct { GFX_2D_SURFACE *surface_pic; GFX_2D_SURFACE *surface_tex[GFX_MAX_TEXTURES]; int32_t texture_map[GFX_MAX_TEXTURES]; - int32_t env_map_texture; int32_t current_texture; } M_PRIV; @@ -130,13 +129,10 @@ static void M_ShadeColor( GFX_3D_VERTEX *const target, uint32_t red, uint32_t green, const uint32_t blue, const uint8_t alpha) { - if (Output_IsShadeEffect()) { - red /= 2; - green = green * 7 / 8; - } - target->r = red; - target->g = green; - target->b = blue; + const RGB_F tint = Output_GetTint(); + target->r = red * tint.r; + target->g = green * tint.g; + target->b = blue * tint.b; target->a = alpha; } @@ -150,22 +146,22 @@ static void M_ShadeLightColor( GFX_3D_VERTEX *const target, uint32_t shade, const bool is_textured, uint32_t red, uint32_t green, uint32_t blue, const uint8_t alpha) { - CLAMPG(shade, 0x1FFF); + CLAMPG(shade, SHADE_MAX); if (g_Config.rendering.lighting_contrast == LIGHTING_CONTRAST_MEDIUM) { CLAMPL(shade, 0x800); } if (g_Config.rendering.lighting_contrast != LIGHTING_CONTRAST_LOW && is_textured) { - shade = 0x1000 + shade / 2; + shade = SHADE_NEUTRAL + shade / 2; } if (g_Config.rendering.lighting_contrast == LIGHTING_CONTRAST_LOW && !is_textured) { - CLAMPL(shade, 0x1000); + CLAMPL(shade, SHADE_NEUTRAL); } - if (shade != 0x1000) { - const int32_t brightness = 0x1FFF - shade; + if (shade != SHADE_NEUTRAL) { + const int32_t brightness = SHADE_MAX - shade; red = (red * brightness) >> 12; green = (green * brightness) >> 12; blue = (blue * brightness) >> 12; @@ -187,10 +183,6 @@ static void M_ReleaseTextures(RENDERER *const renderer) priv->texture_map[i] = GFX_NO_TEXTURE; } } - if (priv->env_map_texture != GFX_NO_TEXTURE) { - GFX_3D_Renderer_UnregisterEnvironmentMap( - priv->renderer_3d, priv->env_map_texture); - } } static void M_LoadTexturePages(RENDERER *renderer) @@ -211,9 +203,6 @@ static void M_LoadTexturePages(RENDERER *renderer) priv->renderer_3d, surface->buffer, surface->desc.width, surface->desc.height); } - - priv->env_map_texture = - GFX_3D_Renderer_RegisterEnvironmentMap(priv->renderer_3d); } static void M_SelectTexture(RENDERER *const renderer, const int32_t tex_source) @@ -1247,7 +1236,6 @@ static void M_Init(RENDERER *const renderer) for (int32_t i = 0; i < GFX_MAX_TEXTURES; i++) { priv->texture_map[i] = GFX_NO_TEXTURE; } - priv->env_map_texture = GFX_NO_TEXTURE; renderer->initialized = true; renderer->priv = priv; diff --git a/src/tr2/game/render/swr.c b/src/tr2/game/render/swr.c index 1478bca4f..e0fefc072 100644 --- a/src/tr2/game/render/swr.c +++ b/src/tr2/game/render/swr.c @@ -1439,8 +1439,11 @@ static void M_SetWet(RENDERER *const renderer, const bool is_wet) { M_PRIV *const priv = renderer->priv; if (is_wet) { + const RGB_F tint = Output_GetTint(); GFX_2D_Renderer_SetTint( - priv->renderer_2d, (GFX_COLOR) { .r = 170, .g = 170, .b = 255 }); + priv->renderer_2d, + (GFX_COLOR) { + .r = tint.r * 255, .g = tint.g * 255, .b = tint.b * 255 }); } else { GFX_2D_Renderer_SetTint( priv->renderer_2d, (GFX_COLOR) { .r = 255, .g = 255, .b = 255 }); diff --git a/src/tr2/game/requester.c b/src/tr2/game/requester.c deleted file mode 100644 index 2cc0d98eb..000000000 --- a/src/tr2/game/requester.c +++ /dev/null @@ -1,461 +0,0 @@ -#include "game/requester.h" - -#include "decomp/decomp.h" -#include "game/game_string.h" -#include "game/input.h" -#include "game/text.h" -#include "global/vars.h" - -#include - -static void M_ClearTextStrings(REQUEST_INFO *req); - -static void M_ClearTextStrings(REQUEST_INFO *const req) -{ - Text_Remove(req->heading_text1); - req->heading_text1 = nullptr; - - Text_Remove(req->heading_text2); - req->heading_text2 = nullptr; - - Text_Remove(req->background_text); - req->background_text = nullptr; - - Text_Remove(req->moreup_text); - req->moreup_text = nullptr; - - Text_Remove(req->moredown_text); - req->moredown_text = nullptr; - - for (int32_t i = 0; i < MAX_REQUESTER_ITEMS; i++) { - Text_Remove(req->item_texts1[i]); - req->item_texts1[i] = nullptr; - Text_Remove(req->item_texts2[i]); - req->item_texts2[i] = nullptr; - } -} - -void Requester_Init(REQUEST_INFO *const req) -{ - M_ClearTextStrings(req); - - req->background_flags = 1; - req->moreup_flags = 1; - req->moredown_flags = 1; - req->items_count = 0; - - req->heading_text1 = nullptr; - req->heading_text2 = nullptr; - req->heading_flags1 = 0; - req->heading_flags2 = 0; - req->background_text = nullptr; - req->moreup_text = nullptr; - req->moredown_text = nullptr; - - for (int32_t i = 0; i < MAX_REQUESTER_ITEMS; i++) { - req->item_texts1[i] = nullptr; - req->item_texts2[i] = nullptr; - req->item_flags1[i] = 0; - req->item_flags2[i] = 0; - } - - req->pitem_flags1 = g_RequesterFlags1; - req->pitem_flags2 = g_RequesterFlags2; - - req->render_width = g_PhdWinWidth; - req->render_height = g_PhdWinHeight; -} - -void Requester_Shutdown(REQUEST_INFO *const req) -{ - M_ClearTextStrings(req); - req->ready = 0; -} - -int32_t Requester_Display( - REQUEST_INFO *const req, const bool destroy, const bool backgrounds) -{ - int32_t line_qty = req->visible_count; - const int32_t lines_height = line_qty * req->line_height + 10; - const int32_t line_one_off = req->y_pos - lines_height; - - const uint32_t render_width = g_PhdWinWidth; - const uint32_t render_height = g_PhdWinHeight; - if (render_width != req->render_width - || render_height != req->render_height) { - Requester_Shutdown(req); - req->render_width = render_width; - req->render_height = render_height; - } - - req->pitem_flags1 = g_RequesterFlags1; - req->pitem_flags2 = g_RequesterFlags2; - if (req->items_count < req->visible_count) { - line_qty = req->items_count; - } - - /* heading */ - if (req->heading_flags1 & REQ_USE) { - if (req->heading_text1 == nullptr) { - req->heading_text1 = Text_Create( - req->x_pos, line_one_off - req->line_height - 10, - req->heading_string1); - req->heading_text1->pos.z = req->z_pos; - Text_CentreH(req->heading_text1, true); - Text_AlignBottom(req->heading_text1, true); - if (backgrounds) { - Text_AddBackground( - req->heading_text1, req->pix_width - 4, 0, 0, 0, - TS_HEADING); - Text_AddOutline(req->heading_text1, TS_HEADING); - } - } - - if (req->heading_flags1 & REQ_ALIGN_LEFT) { - Requester_Item_LeftAlign(req, req->heading_text1); - } - if (req->heading_flags1 & REQ_ALIGN_RIGHT) { - Requester_Item_RightAlign(req, req->heading_text1); - } - } - - /* heading 2 */ - if (req->heading_flags2 & REQ_USE) { - if (req->heading_text2) { - } else { - req->heading_text2 = Text_Create( - req->x_pos, line_one_off - req->line_height - 10, - req->heading_string2); - req->heading_text2->pos.z = req->z_pos; - Text_CentreH(req->heading_text2, true); - Text_AlignBottom(req->heading_text2, true); - if (backgrounds) { - Text_AddBackground( - req->heading_text2, req->pix_width - 4, 0, 0, 0, - TS_HEADING); - Text_AddOutline(req->heading_text2, TS_HEADING); - } - } - - if (req->heading_flags2 & REQ_ALIGN_LEFT) { - Requester_Item_LeftAlign(req, req->heading_text2); - } - - if (req->heading_flags2 & REQ_ALIGN_RIGHT) { - Requester_Item_RightAlign(req, req->heading_text2); - } - } - - /* background */ - if (backgrounds && !req->background_text - && (req->background_flags & 1) != 0) { - req->background_text = - Text_Create(req->x_pos, line_one_off - req->line_height - 12, " "); - req->background_text->pos.z = req->z_pos; - Text_CentreH(req->background_text, true); - Text_AlignBottom(req->background_text, true); - Text_AddBackground( - req->background_text, req->pix_width, - req->line_height + lines_height + 12, 0, 0, TS_BACKGROUND); - Text_AddOutline(req->background_text, TS_BACKGROUND); - } - - /* arrows */ - if (req->line_offset) { - if (!req->moreup_text && (req->moreup_flags & 1) != 0) { - Text_CentreH(req->moreup_text, true); - Text_AlignBottom(req->moreup_text, true); - } - } else { - Text_Remove(req->moreup_text); - req->moreup_text = nullptr; - } - - if (req->items_count <= req->visible_count + req->line_offset) { - Text_Remove(req->moredown_text); - req->moredown_text = nullptr; - } else if (!req->moredown_text && (req->moredown_flags & 1) != 0) { - Text_CentreH(req->moredown_text, true); - Text_AlignBottom(req->moredown_text, true); - } - - /* item texts */ - for (int32_t i = 0; i < line_qty; i++) { - const int32_t n = i + req->line_offset; - - TEXTSTRING **const text1 = &req->item_texts1[i]; - if (req->pitem_flags1[n] & REQ_USE) { - if (*text1 == nullptr) { - *text1 = Text_Create( - 0, line_one_off + req->line_height * i, - &req->pitem_strings1[n * req->item_string_len]); - (*text1)->pos.z = req->z_pos; - Text_CentreH(*text1, true); - Text_AlignBottom(*text1, true); - } - if (req->no_selector || n != req->selected) { - Text_RemoveBackground(*text1); - Text_RemoveOutline(*text1); - } else { - Text_AddBackground( - *text1, req->pix_width - 12, 0, 0, 0, TS_REQUESTED); - Text_AddOutline(*text1, TS_REQUESTED); - } - - if (req->pitem_flags1[n] & REQ_ALIGN_LEFT) { - Requester_Item_LeftAlign(req, *text1); - } else if (req->pitem_flags1[n] & REQ_ALIGN_RIGHT) { - Requester_Item_RightAlign(req, *text1); - } else { - Requester_Item_CenterAlign(req, *text1); - } - } else { - Text_Remove(*text1); - Text_RemoveBackground(*text1); - Text_RemoveOutline(*text1); - *text1 = nullptr; - } - - TEXTSTRING **const text2 = &req->item_texts2[i]; - if (req->pitem_flags2[n] & REQ_USE) { - if (*text2 == nullptr) { - *text2 = Text_Create( - 0, line_one_off + req->line_height * i, - &req->pitem_strings2[n * req->item_string_len]); - (*text2)->pos.z = req->z_pos; - Text_CentreH(*text2, true); - Text_AlignBottom(*text2, true); - } - if (req->pitem_flags2[n] & REQ_ALIGN_LEFT) { - Requester_Item_LeftAlign(req, *text2); - } else if (req->pitem_flags2[n] & REQ_ALIGN_RIGHT) { - Requester_Item_RightAlign(req, *text2); - } else { - Requester_Item_CenterAlign(req, *text2); - } - } else { - Text_Remove(*text2); - Text_RemoveBackground(*text2); - Text_RemoveOutline(*text2); - *text2 = nullptr; - } - } - - if (req->line_offset != req->line_old_offset) { - for (int32_t i = 0; i < line_qty; i++) { - const int32_t n = req->line_offset + i; - - TEXTSTRING **text1 = &req->item_texts1[i]; - if (*text1 != nullptr && (req->pitem_flags1[n] & REQ_USE)) { - Text_ChangeText( - *text1, &req->pitem_strings1[n * req->item_string_len]); - } - if (req->pitem_flags1[n] & REQ_ALIGN_LEFT) { - Requester_Item_LeftAlign(req, *text1); - } else if (req->pitem_flags1[n] & REQ_ALIGN_RIGHT) { - Requester_Item_RightAlign(req, *text1); - } else { - Requester_Item_CenterAlign(req, *text1); - } - - TEXTSTRING **text2 = &req->item_texts2[i]; - if (*text2 != nullptr && (req->pitem_flags2[n] & REQ_USE)) { - Text_ChangeText( - *text2, &req->pitem_strings2[n * req->item_string_len]); - } - if (req->pitem_flags2[n] & REQ_ALIGN_LEFT) { - Requester_Item_LeftAlign(req, *text2); - } else if (req->pitem_flags2[n] & REQ_ALIGN_RIGHT) { - Requester_Item_RightAlign(req, *text2); - } else { - Requester_Item_CenterAlign(req, *text2); - } - } - } - - if (g_InputDB.menu_down) { - if (req->no_selector) { - req->line_old_offset = req->line_offset; - if (req->visible_count + req->line_offset < req->items_count) { - req->line_offset++; - return 0; - } - } else { - if (req->selected < req->items_count - 1) { - req->selected++; - } - req->line_old_offset = req->line_offset; - if (req->selected > req->line_offset + req->visible_count - 1) { - req->line_offset++; - return 0; - } - } - return 0; - } - - if (g_InputDB.menu_up) { - if (req->no_selector) { - req->line_old_offset = req->line_offset; - if (req->line_offset > 0) { - req->line_offset--; - return 0; - } - } else { - if (req->selected > 0) { - req->selected--; - } - req->line_old_offset = req->line_offset; - if (req->selected < req->line_offset) { - req->line_offset--; - return 0; - } - } - return 0; - } - - if (!g_InputDB.menu_confirm) { - if (g_InputDB.menu_back && destroy) { - Requester_Shutdown(req); - return -1; - } - return 0; - } - - if (destroy) { - Requester_Shutdown(req); - } - return req->selected + 1; -} - -void Requester_RemoveAllItems(REQUEST_INFO *const req) -{ - req->items_count = 0; - req->line_offset = 0; - req->selected = 0; -} - -void Requester_Item_CenterAlign(REQUEST_INFO *const req, TEXTSTRING *const text) -{ - if (text == nullptr) { - return; - } - text->background.offset.x = 0; - text->pos.x = req->x_pos; -} - -void Requester_Item_LeftAlign(REQUEST_INFO *const req, TEXTSTRING *const text) -{ - if (text == nullptr) { - return; - } - const int32_t x = - (req->pix_width - - (Text_GetWidth(text) * TEXT_BASE_SCALE / text->scale.h)) - / 2 - - 8; - text->pos.x = req->x_pos - x; - text->background.offset.x = x; -} - -void Requester_Item_RightAlign(REQUEST_INFO *const req, TEXTSTRING *const text) -{ - if (text == nullptr) { - return; - } - const int32_t x = - (req->pix_width - - (Text_GetWidth(text) * TEXT_BASE_SCALE / text->scale.h)) - / 2 - - 8; - text->pos.x = req->x_pos + x; - text->background.offset.x = -x; -} - -void Requester_SetHeading( - REQUEST_INFO *const req, const char *const text1, const uint32_t flags1, - const char *const text2, const uint32_t flags2) -{ - Text_Remove(req->heading_text1); - req->heading_text1 = nullptr; - - Text_Remove(req->heading_text2); - req->heading_text2 = nullptr; - - if (text1 != nullptr) { - strcpy(req->heading_string1, text1); - req->heading_flags1 = flags1 | REQ_USE; - } else { - strcpy(req->heading_string1, "u"); - req->heading_flags1 = 0; - } - - if (text2 != nullptr) { - strcpy(req->heading_string2, text2); - req->heading_flags2 = flags2 | REQ_USE; - } else { - strcpy(req->heading_string2, "u"); - req->heading_flags2 = 0; - } -} - -void Requester_ChangeItem( - REQUEST_INFO *const req, const int32_t item, const char *const text1, - const uint32_t flags1, const char *const text2, const uint32_t flags2) -{ - Text_Remove(req->item_texts1[item]); - req->item_texts1[item] = nullptr; - - Text_Remove(req->item_texts2[item]); - req->item_texts2[item] = nullptr; - - if (text1 != nullptr) { - strcpy(&req->pitem_strings1[item * req->item_string_len], text1); - req->pitem_flags1[item] = flags1 | REQ_USE; - } else { - req->pitem_flags1[item] = 0; - } - - if (text2 != nullptr) { - strcpy(&req->pitem_strings2[item * req->item_string_len], text2); - req->pitem_flags2[item] = flags2 | REQ_USE; - } else { - req->pitem_flags2[item] = 0; - } -} - -void Requester_AddItem( - REQUEST_INFO *const req, const char *const text1, const uint32_t flags1, - const char *const text2, const uint32_t flags2) -{ - req->pitem_flags1 = g_RequesterFlags1; - req->pitem_flags2 = g_RequesterFlags2; - - if (text1 != nullptr) { - strcpy( - &req->pitem_strings1[req->items_count * req->item_string_len], - text1); - req->pitem_flags1[req->items_count] = flags1 | REQ_USE; - } else { - g_RequesterFlags1[req->items_count] = 0; - } - - if (text2 != nullptr) { - strcpy( - &req->pitem_strings2[req->items_count * req->item_string_len], - text2); - req->pitem_flags2[req->items_count] = flags2 | REQ_USE; - } else { - req->pitem_flags2[req->items_count] = 0; - } - - req->items_count++; -} - -void Requester_SetSize( - REQUEST_INFO *const req, const int32_t max_lines, const int32_t y_pos) -{ - int32_t visible_lines = g_PhdWinHeight / 2 / 18; - CLAMPG(visible_lines, max_lines); - req->y_pos = y_pos; - req->visible_count = visible_lines; -} diff --git a/src/tr2/game/requester.h b/src/tr2/game/requester.h deleted file mode 100644 index a26473d22..000000000 --- a/src/tr2/game/requester.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "global/types.h" - -void Requester_Init(REQUEST_INFO *req); -void Requester_Shutdown(REQUEST_INFO *req); -int32_t Requester_Display(REQUEST_INFO *req, bool destroy, bool backgrounds); -void Requester_RemoveAllItems(REQUEST_INFO *req); -void Requester_Item_CenterAlign(REQUEST_INFO *req, TEXTSTRING *text); -void Requester_Item_LeftAlign(REQUEST_INFO *req, TEXTSTRING *text); -void Requester_Item_RightAlign(REQUEST_INFO *req, TEXTSTRING *text); -void Requester_SetHeading( - REQUEST_INFO *req, const char *text1, uint32_t flags1, const char *text2, - uint32_t flags2); -void Requester_ChangeItem( - REQUEST_INFO *req, int32_t item, const char *text1, uint32_t flags1, - const char *text2, uint32_t flags2); -void Requester_AddItem( - REQUEST_INFO *req, const char *text1, uint32_t flags1, const char *text2, - uint32_t flags2); -void Requester_SetSize(REQUEST_INFO *req, int32_t max_lines, int32_t y_pos); diff --git a/src/tr2/game/room.h b/src/tr2/game/room.h index 09de7087d..9062cf960 100644 --- a/src/tr2/game/room.h +++ b/src/tr2/game/room.h @@ -11,7 +11,5 @@ void Room_GetNewRoom(int32_t x, int32_t y, int32_t z, int16_t room_num); // TODO: poor abstraction void Room_InitCinematic(void); -int32_t Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num); - void Room_TestTriggers(const ITEM *item); void Room_TestSectorTrigger(const ITEM *item, const SECTOR *sector); diff --git a/src/tr2/game/savegame.h b/src/tr2/game/savegame.h new file mode 100644 index 000000000..18cbe44f9 --- /dev/null +++ b/src/tr2/game/savegame.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/src/tr2/game/savegame/common.c b/src/tr2/game/savegame/common.c index 6d3b667ca..892342892 100644 --- a/src/tr2/game/savegame/common.c +++ b/src/tr2/game/savegame/common.c @@ -1,88 +1,133 @@ -#include "decomp/savegame.h" +#include "game/game.h" #include "game/game_flow.h" +#include "game/game_string.h" +#include "game/savegame.h" +#include "global/types_decomp.h" #include "global/vars.h" #include -#include -#include -#include -#include +#include +#include -static STATS_COMMON *m_DefaultStats = nullptr; - -void Savegame_Init(void) -{ - g_SaveGame.start = Memory_Alloc( - sizeof(START_INFO) - * (GF_GetLevelTable(GFLT_MAIN)->count - + GF_GetLevelTable(GFLT_DEMOS)->count)); - - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS); - for (int32_t i = 0; i < level_table->count; i++) { - START_INFO *const resume_info = - Savegame_GetCurrentInfo(&level_table->levels[i]); - resume_info->available = 1; - resume_info->has_pistols = 1; - resume_info->pistol_ammo = 1000; - resume_info->gun_status = LGS_ARMLESS; - resume_info->gun_type = LGT_PISTOLS; - } -} - -void Savegame_Shutdown(void) -{ - Memory_FreePointer(&g_SaveGame.start); - Memory_FreePointer(&m_DefaultStats); -} +// TODO: make configurable +#define MAX_SAVE_SLOTS MAX_REQUESTER_ITEMS int32_t Savegame_GetSlotCount(void) { return MAX_SAVE_SLOTS; } -bool Savegame_IsSlotFree(const int32_t slot_idx) +void Savegame_HighlightNewestSlot(void) { - return g_SavedLevels[slot_idx] == 0; } -bool Savegame_Save(const int32_t slot_idx) +void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) { - CreateSaveGameInfo(); - S_SaveGame(slot_idx); - GetSavedGamesList(&g_LoadGameRequester); - return true; -} - -START_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level) -{ - ASSERT(g_SaveGame.start != nullptr); - ASSERT(level != nullptr); - if (GF_GetLevelTableType(level->type) == GFLT_MAIN) { - return &g_SaveGame.start[level->num]; - } else if (level->type == GFL_DEMO) { - return &g_SaveGame.start[GF_GetLevelTable(GFLT_MAIN)->count]; + RESUME_INFO *resume = Savegame_GetCurrentInfo(level); + if (resume == nullptr) { + return; } - LOG_WARNING( - "Warning: unable to get resume info for level %d (type=%s)", level->num, - ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type)); - return nullptr; -} -void Savegame_SetDefaultStats( - const GF_LEVEL *const level, const STATS_COMMON stats) -{ - if (m_DefaultStats == nullptr) { - m_DefaultStats = Memory_Alloc( - sizeof(STATS_COMMON) * GF_GetLevelTable(GFLT_MAIN)->count); - } - m_DefaultStats[level->num] = stats; -} + resume->flags.has_pistols = 1; + resume->equipped_gun_type = LGT_PISTOLS; + resume->pistol_ammo = 1000; -STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *const level) -{ - if (m_DefaultStats == nullptr - || (level->type != GFL_NORMAL && level->type != GFL_BONUS)) { - return (STATS_COMMON) {}; + if (level == GF_GetGymLevel()) { + resume->flags.available = 1; + + resume->flags.has_pistols = 0; + resume->flags.has_shotgun = 0; + resume->flags.has_magnums = 0; + resume->flags.has_uzis = 0; + resume->flags.has_harpoon = 0; + resume->flags.has_m16 = 0; + resume->flags.has_grenade = 0; + + resume->pistol_ammo = 0; + resume->shotgun_ammo = 0; + resume->magnum_ammo = 0; + resume->uzi_ammo = 0; + resume->harpoon_ammo = 0; + resume->m16_ammo = 0; + resume->grenade_ammo = 0; + + resume->flares = 0; + resume->large_medipacks = 0; + resume->small_medipacks = 0; + resume->equipped_gun_type = LGT_UNARMED; + resume->gun_status = LGS_ARMLESS; + } else if (level == GF_GetFirstLevel()) { + resume->flags.available = 1; + + resume->flags.has_pistols = 1; + resume->flags.has_shotgun = 1; + resume->flags.has_magnums = 0; + resume->flags.has_uzis = 0; + resume->flags.has_harpoon = 0; + resume->flags.has_m16 = 0; + resume->flags.has_grenade = 0; + + resume->shotgun_ammo = 2 * SHOTGUN_AMMO_CLIP; + resume->magnum_ammo = 0; + resume->uzi_ammo = 0; + resume->harpoon_ammo = 0; + resume->m16_ammo = 0; + resume->grenade_ammo = 0; + + resume->flares = 2; + resume->small_medipacks = 1; + resume->large_medipacks = 1; + resume->gun_status = LGS_ARMLESS; } - return m_DefaultStats[level->num]; + + if (Game_IsBonusFlagSet(GBF_NGPLUS) && level != GF_GetGymLevel()) { + resume->flags.has_pistols = 1; + resume->flags.has_shotgun = 1; + resume->flags.has_magnums = 1; + resume->flags.has_uzis = 1; + resume->flags.has_grenade = 1; + resume->flags.has_harpoon = 1; + resume->flags.has_m16 = 1; + resume->flags.has_grenade = 1; + + resume->shotgun_ammo = 10000; + resume->magnum_ammo = 10000; + resume->uzi_ammo = 10000; + resume->harpoon_ammo = 10000; + resume->m16_ammo = 10000; + resume->grenade_ammo = 10000; + + resume->flares = -1; + resume->equipped_gun_type = LGT_GRENADE; + } + + if (g_GF_RemoveWeapons) { + resume->flags.has_pistols = 0; + resume->flags.has_magnums = 0; + resume->flags.has_uzis = 0; + resume->flags.has_shotgun = 0; + resume->flags.has_m16 = 0; + resume->flags.has_grenade = 0; + resume->flags.has_harpoon = 0; + resume->equipped_gun_type = LGT_UNARMED; + resume->gun_status = LGS_ARMLESS; + g_GF_RemoveWeapons = false; + } + + if (g_GF_RemoveAmmo) { + resume->m16_ammo = 0; + resume->grenade_ammo = 0; + resume->harpoon_ammo = 0; + resume->shotgun_ammo = 0; + resume->uzi_ammo = 0; + resume->magnum_ammo = 0; + resume->pistol_ammo = 0; + resume->flares = 0; + resume->large_medipacks = 0; + resume->small_medipacks = 0; + g_GF_RemoveAmmo = false; + } + + const STATS_COMMON default_stats = Savegame_GetDefaultStats(level); + resume->stats.max_secret_count = default_stats.max_secret_count; } diff --git a/src/tr2/game/savegame/savegame_bson.c b/src/tr2/game/savegame/savegame_bson.c new file mode 100644 index 000000000..8727d5191 --- /dev/null +++ b/src/tr2/game/savegame/savegame_bson.c @@ -0,0 +1,1308 @@ +#include "game/game.h" +#include "game/game_flow.h" +#include "game/inventory.h" +#include "game/lara/control.h" +#include "game/music.h" +#include "game/objects/general/lift.h" +#include "game/savegame.h" +#include "global/vars.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define SAVEGAME_BSON_MAGIC MKTAG('T', '2', 'X', 'B') + +#define DUMP_XYZ(obj, key, value) \ + do { \ + JSON_OBJECT *const sub_obj = JSON_ObjectNew(); \ + JSON_ObjectAppendInt(sub_obj, "x", value.x); \ + JSON_ObjectAppendInt(sub_obj, "y", value.y); \ + JSON_ObjectAppendInt(sub_obj, "z", value.z); \ + JSON_ObjectAppendObject(obj, key, sub_obj); \ + } while (0) + +#define LOAD_XYZ(obj, key, value) \ + do { \ + const JSON_OBJECT *const sub_obj = JSON_ObjectGetObject(obj, key); \ + value.x = JSON_ObjectGetInt(sub_obj, "x", value.x); \ + value.y = JSON_ObjectGetInt(sub_obj, "y", value.y); \ + value.z = JSON_ObjectGetInt(sub_obj, "z", value.z); \ + } while (0) + +static void M_SaveRaw(MYFILE *fp, const JSON_VALUE *root); +static JSON_VALUE *M_ReadRaw(MYFILE *fp, int32_t *version_out); +static JSON_VALUE *M_ParseFromBuffer(const char *buffer, int32_t *version_out); + +static JSON_OBJECT *M_DumpMisc(void); +static JSON_OBJECT *M_DumpMusic(void); +static JSON_ARRAY *M_DumpResumeInfo(void); +static JSON_OBJECT *M_DumpInventory(void); +static JSON_OBJECT *M_DumpFlipmaps(void); +static JSON_ARRAY *M_DumpCameras(void); +static JSON_ARRAY *M_DumpItems(void); +static JSON_ARRAY *M_DumpFlares(void); +static JSON_OBJECT *M_DumpLara(void); +static JSON_OBJECT *M_DumpArm(const LARA_ARM *arm); +static JSON_OBJECT *M_DumpAmmo(const AMMO_INFO *ammo); + +static bool M_LoadMisc(JSON_OBJECT *misc_obj); +static bool M_LoadMusic(JSON_OBJECT *music_obj); +static bool M_LoadResumeInfo(JSON_ARRAY *resume_arr); +static bool M_LoadInventory(JSON_OBJECT *inv_obj); +static bool M_LoadFlipmaps(JSON_OBJECT *flipmap_obj); +static bool M_LoadCameras(JSON_ARRAY *cameras_arr); +static bool M_LoadItems(JSON_ARRAY *items_arr); +static bool M_LoadFlares(JSON_ARRAY *flares_arr); +static bool M_LoadLara(JSON_OBJECT *lara_obj); +static bool M_LoadArm(JSON_OBJECT *arm_obj, LARA_ARM *arm); +static bool M_LoadAmmo(JSON_OBJECT *ammo_obj, AMMO_INFO *ammo); + +static bool M_IsValidItemObject( + GAME_OBJECT_ID saved_obj_id, GAME_OBJECT_ID initial_obj_id); + +static const char *M_GetSaveFilePattern(void); +static bool M_FillInfo(MYFILE *fp, SAVEGAME_INFO *info); +static void M_SaveToFile(MYFILE *fp, SAVEGAME_INFO *info); +static bool M_LoadFromFile(MYFILE *fp); + +static SAVEGAME_STRATEGY m_Strategy = { + // clang-format off + .allow_load = true, + .allow_save = true, + .format = SAVEGAME_FORMAT_BSON, + .get_save_file_pattern_func = M_GetSaveFilePattern, + .fill_info_func = M_FillInfo, + .load_from_file_func = M_LoadFromFile, + .save_to_file_func = M_SaveToFile, + .load_only_resume_info_func = nullptr, + .update_death_counters_func = nullptr, + // clang-format on +}; + +static const char *M_GetSaveFilePattern(void) +{ + return g_GameFlow.savegame_fmt_bson; +} + +static bool M_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const info) +{ + SAVEGAME_BSON_HEADER header; + File_Seek(fp, 0, FILE_SEEK_SET); + File_ReadData(fp, &header, sizeof(SAVEGAME_BSON_HEADER)); + info->initial_version = header.initial_version; + info->features.restart = false; + info->features.select_level = false; + + File_Skip(fp, header.compressed_size); + SAVEGAME_BSON_EXTENDED_HEADER extra_header; + File_ReadData(fp, &extra_header, sizeof(extra_header)); + + info->counter = extra_header.counter; + info->level_num = extra_header.level_num; + if (extra_header.title_size >= (int32_t)File_Size(fp)) { + return false; + } + info->level_title = Memory_Alloc(extra_header.title_size + 1); + File_ReadData(fp, info->level_title, extra_header.title_size); + return true; +} + +static void M_SaveToFile(MYFILE *const fp, SAVEGAME_INFO *const info) +{ + const GF_LEVEL *const current_level = Game_GetCurrentLevel(); + JSON_OBJECT *const root_obj = JSON_ObjectNew(); + + JSON_ObjectAppendObject(root_obj, "misc", M_DumpMisc()); + JSON_ObjectAppendObject(root_obj, "music", M_DumpMusic()); + JSON_ObjectAppendArray(root_obj, "resume_info", M_DumpResumeInfo()); + JSON_ObjectAppendObject(root_obj, "inventory", M_DumpInventory()); + JSON_ObjectAppendObject(root_obj, "flipmap", M_DumpFlipmaps()); + JSON_ObjectAppendArray(root_obj, "cameras", M_DumpCameras()); + JSON_ObjectAppendArray(root_obj, "items", M_DumpItems()); + JSON_ObjectAppendArray(root_obj, "flares", M_DumpFlares()); + JSON_ObjectAppendObject(root_obj, "lara", M_DumpLara()); + + JSON_VALUE *const root = JSON_ValueFromObject(root_obj); + M_SaveRaw(fp, root); + JSON_ValueFree(root); +} + +static void M_SaveRaw(MYFILE *const fp, const JSON_VALUE *const root) +{ + size_t uncompressed_size; + char *uncompressed = BSON_Write(root, &uncompressed_size); + + uLongf compressed_size = compressBound(uncompressed_size); + char *compressed = Memory_Alloc(compressed_size); + const int32_t result = compress( + (Bytef *)compressed, &compressed_size, (const Bytef *)uncompressed, + (uLongf)uncompressed_size); + if (result != Z_OK) { + Shell_ExitSystem("Failed to compress savegame data"); + } + + const GF_LEVEL *const level = Game_GetCurrentLevel(); + const SAVEGAME_BSON_HEADER header = { + .magic = SAVEGAME_BSON_MAGIC, + .initial_version = Savegame_GetInitialVersion(), + .version = SAVEGAME_CURRENT_VERSION, + .compressed_size = compressed_size, + .uncompressed_size = uncompressed_size, + }; + const SAVEGAME_BSON_EXTENDED_HEADER extra_header = { + .flags = Game_GetBonusFlag(), + .counter = Savegame_GetCounter(), + .level_num = level->num, + .title_size = strlen(level->title), + }; + + File_WriteData(fp, &header, sizeof(header)); + File_WriteData(fp, compressed, compressed_size); + File_WriteData(fp, &extra_header, sizeof(extra_header)); + File_WriteData(fp, level->title, strlen(level->title)); + + Memory_FreePointer(&uncompressed); + Memory_FreePointer(&compressed); +} + +static bool M_LoadFromFile(MYFILE *const fp) +{ + bool result = false; + + int32_t version; + JSON_VALUE *const root = M_ReadRaw(fp, &version); + JSON_OBJECT *const root_obj = JSON_ValueAsObject(root); + if (root_obj == nullptr) { + LOG_ERROR("Malformed save: cannot parse BSON data"); + goto cleanup; + } + + if (!M_LoadMisc(JSON_ObjectGetObject(root_obj, "misc"))) { + goto cleanup; + } + + if (!M_LoadMusic(JSON_ObjectGetObject(root_obj, "music"))) { + goto cleanup; + } + + if (!M_LoadResumeInfo(JSON_ObjectGetArray(root_obj, "resume_info"))) { + goto cleanup; + } + + if (!M_LoadInventory(JSON_ObjectGetObject(root_obj, "inventory"))) { + goto cleanup; + } + + if (!M_LoadFlipmaps(JSON_ObjectGetObject(root_obj, "flipmap"))) { + goto cleanup; + } + + if (!M_LoadCameras(JSON_ObjectGetArray(root_obj, "cameras"))) { + goto cleanup; + } + + if (!M_LoadItems(JSON_ObjectGetArray(root_obj, "items"))) { + goto cleanup; + } + + if (!M_LoadFlares(JSON_ObjectGetArray(root_obj, "flares"))) { + goto cleanup; + } + + if (!M_LoadLara(JSON_ObjectGetObject(root_obj, "lara"))) { + goto cleanup; + } + + result = true; + +cleanup: + JSON_ValueFree(root); + return result; +} + +static JSON_VALUE *M_ReadRaw(MYFILE *const fp, int32_t *const version_out) +{ + const size_t buffer_size = File_Size(fp); + char *buffer = Memory_Alloc(buffer_size); + File_Seek(fp, 0, FILE_SEEK_SET); + File_ReadData(fp, buffer, buffer_size); + + JSON_VALUE *const result = M_ParseFromBuffer(buffer, version_out); + Memory_FreePointer(&buffer); + return result; +} + +static JSON_VALUE *M_ParseFromBuffer( + const char *const buffer, int32_t *const version_out) +{ + const SAVEGAME_BSON_HEADER *const header = (SAVEGAME_BSON_HEADER *)buffer; + if (header->magic != SAVEGAME_BSON_MAGIC) { + LOG_ERROR("Invalid savegame magic"); + return nullptr; + } + + if (version_out != nullptr) { + *version_out = header->version; + } + + const char *compressed = buffer + sizeof(SAVEGAME_BSON_HEADER); + char *uncompressed = Memory_Alloc(header->uncompressed_size); + + uLongf uncompressed_size = header->uncompressed_size; + const int32_t error_code = uncompress( + (Bytef *)uncompressed, &uncompressed_size, (const Bytef *)compressed, + (uLongf)header->compressed_size); + if (error_code != Z_OK) { + LOG_ERROR("Failed to decompress the data (error %d)", error_code); + Memory_FreePointer(&uncompressed); + return nullptr; + } + + JSON_VALUE *const root = BSON_Parse(uncompressed, uncompressed_size); + Memory_FreePointer(&uncompressed); + return root; +} + +static JSON_OBJECT *M_DumpMisc(void) +{ + JSON_OBJECT *const misc_obj = JSON_ObjectNew(); + JSON_ObjectAppendString(misc_obj, "game_version", g_TRXVersion); + JSON_ObjectAppendInt(misc_obj, "bonus_flag", Game_GetBonusFlag()); + JSON_ObjectAppendBool( + misc_obj, "are_monks_angry", Creature_AreAlliesHostile()); + return misc_obj; +} + +static bool M_LoadMisc(JSON_OBJECT *const misc_obj) +{ + if (misc_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing misc info"); + return false; + } + + const int32_t bonus_flag = JSON_ObjectGetInt(misc_obj, "bonus_flag", 0); + Game_SetBonusFlag(bonus_flag); + const bool hostile = JSON_ObjectGetBool(misc_obj, "are_monks_angry", false); + Creature_SetAlliesHostile(hostile); + return true; +} + +static JSON_OBJECT *M_DumpMusic(void) +{ + JSON_OBJECT *const music_obj = JSON_ObjectNew(); + JSON_ARRAY *const track_arr = JSON_ArrayNew(); + for (int32_t i = 0; i < MAX_MUSIC_TRACKS; i++) { + JSON_ArrayAppendInt(track_arr, Music_GetTrackFlags(i)); + } + JSON_ObjectAppendArray(music_obj, "flags", track_arr); + + const MUSIC_TRACK_ID current_track = Music_GetCurrentPlayingTrack(); + const bool is_ambient = current_track == Music_GetCurrentLoopedTrack(); + JSON_OBJECT *const current_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(current_obj, "current_track", current_track); + JSON_ObjectAppendDouble(current_obj, "timestamp", Music_GetTimestamp()); + JSON_ObjectAppendBool(current_obj, "is_ambient", is_ambient); + JSON_ObjectAppendObject(music_obj, "current", current_obj); + + return music_obj; +} + +static bool M_LoadMusic(JSON_OBJECT *const music_obj) +{ + if (music_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing music info"); + return false; + } + + const JSON_ARRAY *const track_arr = JSON_ObjectGetArray(music_obj, "flags"); + if (track_arr == nullptr) { + LOG_WARNING("Malformed save: invalid or missing music track array"); + return true; + } + + if ((signed)track_arr->length != MAX_MUSIC_TRACKS) { + LOG_WARNING( + "Malformed save: expected %d music track flags, got %d", + MAX_MUSIC_TRACKS, track_arr->length); + return true; + } + + for (int32_t i = 0; i < (signed)track_arr->length; i++) { + Music_SetTrackFlags(i, JSON_ArrayGetInt(track_arr, i, 0)); + } + + const JSON_OBJECT *const current_obj = + JSON_ObjectGetObject(music_obj, "current"); + if (current_obj == nullptr + || g_Config.audio.music_load_condition == MUSIC_LOAD_NEVER) { + return true; + } + + const int16_t current_track = + JSON_ObjectGetInt(current_obj, "current_track", -1); + double timestamp = JSON_ObjectGetDouble(current_obj, "timestamp", -1.0); + if (current_track != MX_INACTIVE) { + const bool is_ambient = + JSON_ObjectGetBool(music_obj, "is_ambient", false); + if (is_ambient) { + if (g_Config.audio.music_load_condition == MUSIC_LOAD_NON_AMBIENT) { + return true; + } + Music_Play(current_track, MPM_LOOPED); + } else { + Music_Play(current_track, MPM_ALWAYS); + } + + if (!Music_SeekTimestamp(timestamp)) { + LOG_WARNING( + "Could not load current track %d at timestamp %" PRId64 ".", + current_track, timestamp); + } + } + + return true; +} + +static JSON_ARRAY *M_DumpResumeInfo(void) +{ + JSON_ARRAY *const resume_arr = JSON_ArrayNew(); + for (int32_t i = 0; i < GF_GetLevelTable(GFLT_MAIN)->count; i++) { + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + const RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + JSON_OBJECT *const resume_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(resume_obj, "pistol_ammo", resume->pistol_ammo); + JSON_ObjectAppendInt(resume_obj, "magnum_ammo", resume->magnum_ammo); + JSON_ObjectAppendInt(resume_obj, "uzi_ammo", resume->uzi_ammo); + JSON_ObjectAppendInt(resume_obj, "shotgun_ammo", resume->shotgun_ammo); + JSON_ObjectAppendInt(resume_obj, "m16_ammo", resume->m16_ammo); + JSON_ObjectAppendInt(resume_obj, "grenade_ammo", resume->grenade_ammo); + JSON_ObjectAppendInt(resume_obj, "harpoon_ammo", resume->harpoon_ammo); + JSON_ObjectAppendInt(resume_obj, "num_medis", resume->small_medipacks); + JSON_ObjectAppendInt( + resume_obj, "num_big_medis", resume->large_medipacks); + JSON_ObjectAppendInt(resume_obj, "num_flares", resume->flares); + JSON_ObjectAppendInt(resume_obj, "gun_status", resume->gun_status); + JSON_ObjectAppendInt(resume_obj, "gun_type", resume->equipped_gun_type); + + JSON_ObjectAppendBool(resume_obj, "available", resume->flags.available); + JSON_ObjectAppendBool( + resume_obj, "has_pistols", resume->flags.has_pistols); + JSON_ObjectAppendBool( + resume_obj, "has_magnums", resume->flags.has_magnums); + JSON_ObjectAppendBool(resume_obj, "has_uzis", resume->flags.has_uzis); + JSON_ObjectAppendBool( + resume_obj, "has_shotgun", resume->flags.has_shotgun); + JSON_ObjectAppendBool(resume_obj, "has_m16", resume->flags.has_m16); + JSON_ObjectAppendBool( + resume_obj, "has_grenade", resume->flags.has_grenade); + JSON_ObjectAppendBool( + resume_obj, "has_harpoon", resume->flags.has_harpoon); + + JSON_ObjectAppendInt(resume_obj, "timer", resume->stats.timer); + JSON_ObjectAppendInt(resume_obj, "ammo_hits", resume->stats.ammo_hits); + JSON_ObjectAppendInt(resume_obj, "ammo_used", resume->stats.ammo_used); + JSON_ObjectAppendInt( + resume_obj, "distance_travelled", resume->stats.distance_travelled); + JSON_ObjectAppendInt(resume_obj, "kills", resume->stats.kill_count); + JSON_ObjectAppendInt(resume_obj, "secrets", resume->stats.secret_flags); + JSON_ObjectAppendDouble( + resume_obj, "medipacks_used", resume->stats.medipacks_used); + JSON_ObjectAppendInt( + resume_obj, "max_secrets", resume->stats.max_secret_count); + JSON_ArrayAppendObject(resume_arr, resume_obj); + } + return resume_arr; +} + +static bool M_LoadResumeInfo(JSON_ARRAY *const resume_arr) +{ + if (resume_arr == nullptr) { + LOG_ERROR("Malformed save: invalid or missing resume array"); + return false; + } + const int32_t expected_length = GF_GetLevelTable(GFLT_MAIN)->count; + if ((signed)resume_arr->length != expected_length) { + LOG_ERROR( + "Malformed save: expected %d resume info elements, got %d", + expected_length, resume_arr->length); + return false; + } + + for (int32_t i = 0; i < expected_length; i++) { + JSON_OBJECT *const resume_obj = JSON_ArrayGetObject(resume_arr, i); + if (resume_obj == nullptr) { + LOG_ERROR("Malformed save: invalid resume info"); + return false; + } + + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, i); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + resume->pistol_ammo = JSON_ObjectGetInt(resume_obj, "pistol_ammo", 0); + resume->magnum_ammo = JSON_ObjectGetInt(resume_obj, "magnum_ammo", 0); + resume->uzi_ammo = JSON_ObjectGetInt(resume_obj, "uzi_ammo", 0); + resume->shotgun_ammo = JSON_ObjectGetInt(resume_obj, "shotgun_ammo", 0); + resume->m16_ammo = JSON_ObjectGetInt(resume_obj, "m16_ammo", 0); + resume->grenade_ammo = JSON_ObjectGetInt(resume_obj, "grenade_ammo", 0); + resume->harpoon_ammo = JSON_ObjectGetInt(resume_obj, "harpoon_ammo", 0); + resume->small_medipacks = JSON_ObjectGetInt(resume_obj, "num_medis", 0); + resume->large_medipacks = + JSON_ObjectGetInt(resume_obj, "num_big_medis", 0); + resume->flares = JSON_ObjectGetInt(resume_obj, "num_flares", 0); + resume->gun_status = + JSON_ObjectGetInt(resume_obj, "gun_status", LGS_ARMLESS); + resume->equipped_gun_type = + JSON_ObjectGetInt(resume_obj, "gun_type", LGT_UNARMED); + + resume->flags.available = + JSON_ObjectGetBool(resume_obj, "available", 0); + resume->flags.has_pistols = + JSON_ObjectGetBool(resume_obj, "has_pistols", 0); + resume->flags.has_magnums = + JSON_ObjectGetBool(resume_obj, "has_magnums", 0); + resume->flags.has_uzis = JSON_ObjectGetBool(resume_obj, "has_uzis", 0); + resume->flags.has_shotgun = + JSON_ObjectGetBool(resume_obj, "has_shotgun", 0); + resume->flags.has_m16 = JSON_ObjectGetBool(resume_obj, "has_m16", 0); + resume->flags.has_grenade = + JSON_ObjectGetBool(resume_obj, "has_grenade", 0); + resume->flags.has_harpoon = + JSON_ObjectGetBool(resume_obj, "has_harpoon", 0); + + resume->stats.timer = JSON_ObjectGetInt(resume_obj, "timer", 0); + resume->stats.ammo_hits = JSON_ObjectGetInt(resume_obj, "ammo_hits", 0); + resume->stats.ammo_used = JSON_ObjectGetInt(resume_obj, "ammo_used", 0); + resume->stats.distance_travelled = + JSON_ObjectGetInt(resume_obj, "distance_travelled", 0); + resume->stats.kill_count = JSON_ObjectGetInt(resume_obj, "kills", 0); + resume->stats.secret_flags = + JSON_ObjectGetInt(resume_obj, "secrets", 0); + resume->stats.medipacks_used = + JSON_ObjectGetDouble(resume_obj, "medipacks_used", 0); + resume->stats.max_secret_count = + JSON_ObjectGetInt(resume_obj, "max_secrets", 0); + } + + return true; +} + +static JSON_OBJECT *M_DumpInventory(void) +{ + JSON_OBJECT *const inv_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(inv_obj, "pickup1", Inv_RequestItem(O_PICKUP_ITEM_1)); + JSON_ObjectAppendInt(inv_obj, "pickup2", Inv_RequestItem(O_PICKUP_ITEM_2)); + JSON_ObjectAppendInt(inv_obj, "puzzle1", Inv_RequestItem(O_PUZZLE_ITEM_1)); + JSON_ObjectAppendInt(inv_obj, "puzzle2", Inv_RequestItem(O_PUZZLE_ITEM_2)); + JSON_ObjectAppendInt(inv_obj, "puzzle3", Inv_RequestItem(O_PUZZLE_ITEM_3)); + JSON_ObjectAppendInt(inv_obj, "puzzle4", Inv_RequestItem(O_PUZZLE_ITEM_4)); + JSON_ObjectAppendInt(inv_obj, "key1", Inv_RequestItem(O_KEY_ITEM_1)); + JSON_ObjectAppendInt(inv_obj, "key2", Inv_RequestItem(O_KEY_ITEM_2)); + JSON_ObjectAppendInt(inv_obj, "key3", Inv_RequestItem(O_KEY_ITEM_3)); + JSON_ObjectAppendInt(inv_obj, "key4", Inv_RequestItem(O_KEY_ITEM_4)); + return inv_obj; +} + +static bool M_LoadInventory(JSON_OBJECT *const inv_obj) +{ + if (inv_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing inventory info"); + return false; + } + + const GF_LEVEL *const current_level = Game_GetCurrentLevel(); + Lara_InitialiseInventory(current_level); + Inv_AddItemNTimes( + O_PICKUP_ITEM_1, JSON_ObjectGetInt(inv_obj, "pickup1", 0)); + Inv_AddItemNTimes( + O_PICKUP_ITEM_2, JSON_ObjectGetInt(inv_obj, "pickup2", 0)); + Inv_AddItemNTimes( + O_PUZZLE_ITEM_1, JSON_ObjectGetInt(inv_obj, "puzzle1", 0)); + Inv_AddItemNTimes( + O_PUZZLE_ITEM_2, JSON_ObjectGetInt(inv_obj, "puzzle2", 0)); + Inv_AddItemNTimes( + O_PUZZLE_ITEM_3, JSON_ObjectGetInt(inv_obj, "puzzle3", 0)); + Inv_AddItemNTimes( + O_PUZZLE_ITEM_4, JSON_ObjectGetInt(inv_obj, "puzzle4", 0)); + Inv_AddItemNTimes(O_KEY_ITEM_1, JSON_ObjectGetInt(inv_obj, "key1", 0)); + Inv_AddItemNTimes(O_KEY_ITEM_2, JSON_ObjectGetInt(inv_obj, "key2", 0)); + Inv_AddItemNTimes(O_KEY_ITEM_3, JSON_ObjectGetInt(inv_obj, "key3", 0)); + Inv_AddItemNTimes(O_KEY_ITEM_4, JSON_ObjectGetInt(inv_obj, "key4", 0)); + + return true; +} + +static JSON_OBJECT *M_DumpFlipmaps(void) +{ + JSON_OBJECT *const flipmap_obj = JSON_ObjectNew(); + JSON_ObjectAppendBool(flipmap_obj, "status", Room_GetFlipStatus()); + JSON_ObjectAppendInt(flipmap_obj, "effect", Room_GetFlipEffect()); + JSON_ObjectAppendInt(flipmap_obj, "timer", Room_GetFlipTimer()); + JSON_ARRAY *const flipmap_arr = JSON_ArrayNew(); + for (int32_t i = 0; i < MAX_FLIP_MAPS; i++) { + JSON_ArrayAppendInt(flipmap_arr, Room_GetFlipSlotFlags(i) >> 8); + } + JSON_ObjectAppendArray(flipmap_obj, "table", flipmap_arr); + return flipmap_obj; +} + +static bool M_LoadFlipmaps(JSON_OBJECT *const flipmap_obj) +{ + if (flipmap_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing flipmap info"); + return false; + } + + if (JSON_ObjectGetBool(flipmap_obj, "status", false)) { + Room_FlipMap(); + } + + Room_SetFlipEffect(JSON_ObjectGetInt(flipmap_obj, "effect", 0)); + Room_SetFlipTimer(JSON_ObjectGetInt(flipmap_obj, "timer", 0)); + + const JSON_ARRAY *const flipmap_arr = + JSON_ObjectGetArray(flipmap_obj, "table"); + if (flipmap_arr == nullptr) { + LOG_ERROR("Malformed save: invalid or missing flipmap table"); + return false; + } + if ((signed)flipmap_arr->length != MAX_FLIP_MAPS) { + LOG_ERROR( + "Malformed save: expected %d flipmap elements, got %d", + MAX_FLIP_MAPS, flipmap_arr->length); + return false; + } + for (int32_t i = 0; i < (signed)flipmap_arr->length; i++) { + Room_SetFlipSlotFlags(i, JSON_ArrayGetInt(flipmap_arr, i, 0) << 8); + } + + return true; +} + +static JSON_ARRAY *M_DumpCameras(void) +{ + JSON_ARRAY *const cameras_arr = JSON_ArrayNew(); + for (int32_t i = 0; i < Camera_GetFixedObjectCount(); i++) { + const OBJECT_VECTOR *const object = Camera_GetFixedObject(i); + JSON_ArrayAppendInt(cameras_arr, object->flags); + } + return cameras_arr; +} + +static bool M_LoadCameras(JSON_ARRAY *const cameras_arr) +{ + if (cameras_arr == nullptr) { + LOG_ERROR("Malformed save: invalid or missing cameras array"); + return false; + } + + const int32_t num_cameras = Camera_GetFixedObjectCount(); + if ((signed)cameras_arr->length != num_cameras) { + LOG_ERROR( + "Malformed save: expected %d cameras, got %d", num_cameras, + cameras_arr->length); + return false; + } + + for (int32_t i = 0; i < num_cameras; i++) { + OBJECT_VECTOR *const object = Camera_GetFixedObject(i); + object->flags = JSON_ArrayGetInt(cameras_arr, i, 0); + } + + return true; +} + +static JSON_ARRAY *M_DumpItems(void) +{ + Savegame_ProcessItemsBeforeSave(); + + JSON_ARRAY *const items_arr = JSON_ArrayNew(); + for (int32_t i = 0; i < Item_GetLevelCount(); i++) { + JSON_OBJECT *const item_obj = JSON_ObjectNew(); + const ITEM *const item = Item_Get(i); + const OBJECT *const obj = Object_Get(item->object_id); + + JSON_ObjectAppendInt(item_obj, "obj_num", item->object_id); + + if (obj->save_position) { + DUMP_XYZ(item_obj, "pos", item->pos); + DUMP_XYZ(item_obj, "rot", item->rot); + JSON_ObjectAppendInt(item_obj, "room_num", item->room_num); + JSON_ObjectAppendInt(item_obj, "speed", item->speed); + JSON_ObjectAppendInt(item_obj, "fall_speed", item->fall_speed); + } + + if (obj->save_anim) { + JSON_ObjectAppendInt( + item_obj, "current_anim", item->current_anim_state); + JSON_ObjectAppendInt(item_obj, "goal_anim", item->goal_anim_state); + JSON_ObjectAppendInt( + item_obj, "required_anim", item->required_anim_state); + JSON_ObjectAppendInt(item_obj, "anim_num", item->anim_num); + JSON_ObjectAppendInt(item_obj, "frame_num", item->frame_num); + } + + if (obj->save_hitpoints) { + JSON_ObjectAppendInt(item_obj, "hitpoints", item->hit_points); + } + + if (obj->save_flags) { + JSON_ObjectAppendInt(item_obj, "flags", item->flags); + JSON_ObjectAppendInt(item_obj, "status", item->status); + JSON_ObjectAppendBool(item_obj, "active", item->active); + JSON_ObjectAppendBool(item_obj, "gravity", item->gravity); + JSON_ObjectAppendBool(item_obj, "collidable", item->collidable); + JSON_ObjectAppendBool( + item_obj, "intelligent", obj->intelligent && item->data); + JSON_ObjectAppendInt(item_obj, "timer", item->timer); + if (obj->intelligent && item->data != nullptr) { + const CREATURE *const creature = (CREATURE *)item->data; + JSON_ObjectAppendInt( + item_obj, "head_rot", creature->head_rotation); + JSON_ObjectAppendInt( + item_obj, "neck_rot", creature->neck_rotation); + JSON_ObjectAppendInt( + item_obj, "max_turn", creature->maximum_turn); + JSON_ObjectAppendInt( + item_obj, "creature_flags", creature->flags); + JSON_ObjectAppendInt(item_obj, "creature_mood", creature->mood); + } + if (obj->intelligent) { + JSON_ObjectAppendInt( + item_obj, "carried_item", item->carried_item); + } + } + + switch (item->object_id) { + case O_BOAT: { + const BOAT_INFO *const data = (BOAT_INFO *)item->data; + JSON_OBJECT *const data_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(data_obj, "boat_turn", data->boat_turn); + JSON_ObjectAppendInt( + data_obj, "left_fallspeed", data->left_fallspeed); + JSON_ObjectAppendInt( + data_obj, "right_fallspeed", data->right_fallspeed); + JSON_ObjectAppendInt(data_obj, "tilt_angle", data->tilt_angle); + JSON_ObjectAppendInt( + data_obj, "extra_rotation", data->extra_rotation); + JSON_ObjectAppendInt(data_obj, "water", data->water); + JSON_ObjectAppendInt(data_obj, "pitch", data->pitch); + JSON_ObjectAppendObject(item_obj, "data", data_obj); + break; + } + + case O_SKIDOO_FAST: { + const SKIDOO_INFO *const data = (SKIDOO_INFO *)item->data; + JSON_OBJECT *const data_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(data_obj, "track_mesh", data->track_mesh); + JSON_ObjectAppendInt(data_obj, "skidoo_turn", data->skidoo_turn); + JSON_ObjectAppendInt( + data_obj, "left_fallspeed", data->left_fallspeed); + JSON_ObjectAppendInt( + data_obj, "right_fallspeed", data->right_fallspeed); + JSON_ObjectAppendInt( + data_obj, "momentum_angle", data->momentum_angle); + JSON_ObjectAppendInt( + data_obj, "extra_rotation", data->extra_rotation); + JSON_ObjectAppendInt(data_obj, "pitch", data->pitch); + JSON_ObjectAppendObject(item_obj, "data", data_obj); + break; + } + + case O_LIFT: { + const LIFT_INFO *const data = (LIFT_INFO *)item->data; + JSON_OBJECT *const data_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(data_obj, "start_height", data->start_height); + JSON_ObjectAppendInt(data_obj, "wait_time", data->wait_time); + JSON_ObjectAppendObject(item_obj, "data", data_obj); + break; + } + + default: + break; + } + + JSON_ArrayAppendObject(items_arr, item_obj); + } + return items_arr; +} + +static bool M_LoadItems(JSON_ARRAY *const items_arr) +{ + if (items_arr == nullptr) { + LOG_ERROR("Malformed save: invalid or missing items array"); + return false; + } + + const int32_t item_count = Item_GetLevelCount(); + if ((signed)items_arr->length != item_count) { + LOG_ERROR( + "Malformed save: expected %d items, got %d", item_count, + items_arr->length); + return false; + } + + Savegame_ProcessItemsBeforeLoad(); + + for (int32_t i = 0; i < item_count; i++) { + JSON_OBJECT *const item_obj = JSON_ArrayGetObject(items_arr, i); + if (item_obj == nullptr) { + LOG_ERROR("Malformed save: invalid item data"); + return false; + } + + ITEM *const item = Item_Get(i); + const OBJECT *const obj = Object_Get(item->object_id); + + const GAME_OBJECT_ID obj_id = + JSON_ObjectGetInt(item_obj, "obj_num", -1); + if (!M_IsValidItemObject(obj_id, item->object_id)) { + LOG_ERROR( + "Malformed save: expected object %d, got %d", item->object_id, + obj_id); + return false; + } + + if (obj->save_position) { + LOAD_XYZ(item_obj, "pos", item->pos); + LOAD_XYZ(item_obj, "rot", item->rot); + item->speed = JSON_ObjectGetInt(item_obj, "speed", item->speed); + item->fall_speed = + JSON_ObjectGetInt(item_obj, "fall_speed", item->fall_speed); + + int16_t room_num = JSON_ObjectGetInt(item_obj, "room_num", -1); + if (room_num != -1 && item->room_num != room_num) { + Item_NewRoom(i, room_num); + } + } + + if (obj->save_anim) { + item->current_anim_state = JSON_ObjectGetInt( + item_obj, "current_anim", item->current_anim_state); + item->goal_anim_state = + JSON_ObjectGetInt(item_obj, "goal_anim", item->goal_anim_state); + item->required_anim_state = JSON_ObjectGetInt( + item_obj, "required_anim", item->required_anim_state); + item->anim_num = + JSON_ObjectGetInt(item_obj, "anim_num", item->anim_num); + item->frame_num = + JSON_ObjectGetInt(item_obj, "frame_num", item->frame_num); + } + + if (obj->save_hitpoints) { + item->hit_points = + JSON_ObjectGetInt(item_obj, "hitpoints", item->hit_points); + } + + if (obj->save_flags) { + item->flags = JSON_ObjectGetInt(item_obj, "flags", item->flags); + item->timer = JSON_ObjectGetInt(item_obj, "timer", item->timer); + + if ((item->flags & IF_KILLED) != 0) { + Item_Kill(i); + item->status = IS_DEACTIVATED; + } else { + if (JSON_ObjectGetBool(item_obj, "active", item->active) + && !item->active) { + Item_AddActive(i); + } + item->status = + JSON_ObjectGetInt(item_obj, "status", item->status); + item->gravity = + JSON_ObjectGetBool(item_obj, "gravity", item->gravity); + item->collidable = JSON_ObjectGetBool( + item_obj, "collidable", item->collidable); + } + + if (JSON_ObjectGetBool(item_obj, "intelligent", obj->intelligent)) { + LOT_EnableBaddieAI(i, true); + CREATURE *const creature = (CREATURE *)item->data; + if (creature != nullptr) { + creature->head_rotation = JSON_ObjectGetInt( + item_obj, "head_rot", creature->head_rotation); + creature->neck_rotation = JSON_ObjectGetInt( + item_obj, "neck_rot", creature->neck_rotation); + creature->maximum_turn = JSON_ObjectGetInt( + item_obj, "max_turn", creature->maximum_turn); + creature->flags = JSON_ObjectGetInt( + item_obj, "creature_flags", creature->flags); + creature->mood = JSON_ObjectGetInt( + item_obj, "creature_mood", creature->mood); + } + } else if (obj->intelligent) { + item->data = nullptr; + if (item->killed && item->hit_points <= 0 + && !(item->flags & IF_KILLED)) { + item->next_active = Item_GetPrevActive(); + Item_SetPrevActive(i); + } + } + + if (obj->intelligent) { + item->carried_item = + JSON_ObjectGetInt(item_obj, "carried_item", NO_ITEM); + } + + switch (item->object_id) { + case O_BOAT: { + const JSON_OBJECT *const data_obj = + JSON_ObjectGetObject(item_obj, "data"); + if (data_obj == nullptr) { + LOG_ERROR( + "Malformed save: missing boat data for item %d", i); + return false; + } + BOAT_INFO *const data = (BOAT_INFO *)item->data; + data->boat_turn = + JSON_ObjectGetInt(data_obj, "boat_turn", data->boat_turn); + data->left_fallspeed = JSON_ObjectGetInt( + data_obj, "left_fallspeed", data->left_fallspeed); + data->right_fallspeed = JSON_ObjectGetInt( + data_obj, "right_fallspeed", data->right_fallspeed); + data->tilt_angle = + JSON_ObjectGetInt(data_obj, "tilt_angle", data->tilt_angle); + data->extra_rotation = JSON_ObjectGetInt( + data_obj, "extra_rotation", data->extra_rotation); + data->water = JSON_ObjectGetInt(data_obj, "water", data->water); + data->pitch = JSON_ObjectGetInt(data_obj, "pitch", data->pitch); + break; + } + + case O_SKIDOO_FAST: { + const JSON_OBJECT *const data_obj = + JSON_ObjectGetObject(item_obj, "data"); + if (data_obj == nullptr) { + LOG_ERROR( + "Malformed save: missing skidoo data for item %d", i); + return false; + } + SKIDOO_INFO *const data = (SKIDOO_INFO *)item->data; + data->track_mesh = + JSON_ObjectGetInt(data_obj, "track_mesh", data->track_mesh); + data->skidoo_turn = JSON_ObjectGetInt( + data_obj, "skidoo_turn", data->skidoo_turn); + data->left_fallspeed = JSON_ObjectGetInt( + data_obj, "left_fallspeed", data->left_fallspeed); + data->right_fallspeed = JSON_ObjectGetInt( + data_obj, "right_fallspeed", data->right_fallspeed); + data->momentum_angle = JSON_ObjectGetInt( + data_obj, "momentum_angle", data->momentum_angle); + data->extra_rotation = JSON_ObjectGetInt( + data_obj, "extra_rotation", data->extra_rotation); + data->pitch = JSON_ObjectGetInt(data_obj, "pitch", data->pitch); + break; + } + + case O_LIFT: { + const JSON_OBJECT *const data_obj = + JSON_ObjectGetObject(item_obj, "data"); + if (data_obj == nullptr) { + LOG_ERROR( + "Malformed save: missing lift data for item %d", i); + return false; + } + LIFT_INFO *const data = (LIFT_INFO *)item->data; + data->start_height = JSON_ObjectGetInt( + data_obj, "start_height", data->start_height); + data->wait_time = + JSON_ObjectGetInt(data_obj, "wait_time", data->wait_time); + break; + } + + default: + break; + } + + if (obj->handle_save_func != nullptr) { + obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD); + } + } + } + + return true; +} + +static bool M_IsValidItemObject( + const GAME_OBJECT_ID saved_obj_id, const GAME_OBJECT_ID initial_obj_id) +{ + if (saved_obj_id == initial_obj_id) { + return true; + } + + // clang-format off + switch (saved_obj_id) { + // used keyholes + case O_PUZZLE_DONE_1: return initial_obj_id == O_PUZZLE_HOLE_1; + case O_PUZZLE_DONE_2: return initial_obj_id == O_PUZZLE_HOLE_2; + case O_PUZZLE_DONE_3: return initial_obj_id == O_PUZZLE_HOLE_3; + case O_PUZZLE_DONE_4: return initial_obj_id == O_PUZZLE_HOLE_4; + // pickups + case O_PISTOL_AMMO_ITEM: return initial_obj_id == O_PISTOL_ITEM; + case O_SHOTGUN_AMMO_ITEM: return initial_obj_id == O_SHOTGUN_ITEM; + case O_MAGNUM_AMMO_ITEM: return initial_obj_id == O_MAGNUM_ITEM; + case O_UZI_AMMO_ITEM: return initial_obj_id == O_UZI_ITEM; + case O_HARPOON_AMMO_ITEM: return initial_obj_id == O_HARPOON_ITEM; + case O_M16_AMMO_ITEM: return initial_obj_id == O_M16_ITEM; + case O_GRENADE_AMMO_ITEM: return initial_obj_id == O_GRENADE_ITEM; + // skidoo swaps + case O_SKIDOO_FAST: return initial_obj_id == O_SKIDOO_ARMED; + // default + default: return false; + } + // clang-format on +} + +static JSON_ARRAY *M_DumpFlares(void) +{ + JSON_ARRAY *const flares_arr = JSON_ArrayNew(); + for (int32_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) { + const ITEM *const item = Item_Get(item_num); + if (!item->active || item->object_id != O_FLARE_ITEM) { + continue; + } + + JSON_OBJECT *const flare_obj = JSON_ObjectNew(); + DUMP_XYZ(flare_obj, "pos", item->pos); + DUMP_XYZ(flare_obj, "rot", item->rot); + JSON_ObjectAppendInt(flare_obj, "room_num", item->room_num); + JSON_ObjectAppendInt(flare_obj, "speed", item->speed); + JSON_ObjectAppendInt(flare_obj, "fall_speed", item->fall_speed); + JSON_ObjectAppendInt(flare_obj, "age", (intptr_t)item->data); + JSON_ArrayAppendObject(flares_arr, flare_obj); + } + return flares_arr; +} + +static bool M_LoadFlares(JSON_ARRAY *const flares_arr) +{ + if (flares_arr == nullptr) { + LOG_ERROR("Malformed save: invalid or missing flares array"); + return false; + } + + for (int32_t i = 0; i < (signed)flares_arr->length; i++) { + JSON_OBJECT *const flare_obj = JSON_ArrayGetObject(flares_arr, i); + if (flare_obj == nullptr) { + LOG_ERROR("Malformed save: invalid flare data"); + return false; + } + + const int16_t item_num = Item_Create(); + ITEM *const item = Item_Get(item_num); + item->object_id = O_FLARE_ITEM; + LOAD_XYZ(flare_obj, "pos", item->pos); + LOAD_XYZ(flare_obj, "rot", item->rot); + item->room_num = + JSON_ObjectGetInt(flare_obj, "room_num", item->room_num); + item->speed = JSON_ObjectGetInt(flare_obj, "speed", item->speed); + item->fall_speed = + JSON_ObjectGetInt(flare_obj, "fall_speed", item->fall_speed); + Item_Initialise(item_num); + Item_AddActive(item_num); + const int32_t flare_age = JSON_ObjectGetInt(flare_obj, "age", 0); + item->data = (void *)(intptr_t)flare_age; + } + return true; +} + +static JSON_OBJECT *M_DumpLara(void) +{ + const LARA_INFO *const lara = Lara_GetLaraInfo(); + ASSERT(lara != nullptr); + + JSON_OBJECT *const lara_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(lara_obj, "item_number", lara->item_num); + JSON_ObjectAppendInt(lara_obj, "gun_status", lara->gun_status); + JSON_ObjectAppendInt(lara_obj, "gun_type", lara->gun_type); + JSON_ObjectAppendInt(lara_obj, "request_gun_type", lara->request_gun_type); + JSON_ObjectAppendInt(lara_obj, "last_gun_type", lara->last_gun_type); + JSON_ObjectAppendInt(lara_obj, "calc_fall_speed", lara->calc_fall_speed); + JSON_ObjectAppendInt(lara_obj, "water_status", lara->water_status); + JSON_ObjectAppendInt(lara_obj, "climb_status", lara->climb_status); + JSON_ObjectAppendInt(lara_obj, "pose_count", lara->pose_count); + JSON_ObjectAppendInt(lara_obj, "hit_frame", lara->hit_frame); + JSON_ObjectAppendInt(lara_obj, "hit_direction", lara->hit_direction); + JSON_ObjectAppendInt(lara_obj, "air", lara->air); + JSON_ObjectAppendInt(lara_obj, "dive_count", lara->dive_count); + JSON_ObjectAppendInt(lara_obj, "death_count", lara->death_timer); + JSON_ObjectAppendInt(lara_obj, "current_active", lara->current_active); + JSON_ObjectAppendInt(lara_obj, "hit_effect_count", lara->hit_effect_count); + JSON_ObjectAppendInt(lara_obj, "flare_age", lara->flare_age); + JSON_ObjectAppendInt(lara_obj, "vehicle_item_number", lara->skidoo); + JSON_ObjectAppendInt(lara_obj, "back_gun_obj_id", lara->back_gun); + JSON_ObjectAppendInt(lara_obj, "flare_frame", lara->flare_frame); + JSON_ObjectAppendInt(lara_obj, "mesh_effects", lara->mesh_effects); + JSON_ObjectAppendInt( + lara_obj, "water_surface_dist", lara->water_surface_dist); + + JSON_ObjectAppendBool( + lara_obj, "flare_control_left", lara->flare_control_left); + JSON_ObjectAppendBool( + lara_obj, "flare_control_right", lara->flare_control_right); + JSON_ObjectAppendBool(lara_obj, "extra_anim", lara->extra_anim); + JSON_ObjectAppendBool(lara_obj, "look", lara->look); + JSON_ObjectAppendBool(lara_obj, "burn", lara->burn); + + JSON_ARRAY *const lara_meshes_arr = JSON_ArrayNew(); + for (int i = 0; i < LM_NUMBER_OF; i++) { + JSON_ArrayAppendInt( + lara_meshes_arr, Object_GetMeshOffset(lara->mesh_ptrs[i])); + } + JSON_ObjectAppendArray(lara_obj, "meshes", lara_meshes_arr); + + JSON_ObjectAppendInt(lara_obj, "target_angle1", lara->target_angles[0]); + JSON_ObjectAppendInt(lara_obj, "target_angle2", lara->target_angles[1]); + JSON_ObjectAppendInt(lara_obj, "turn_rate", lara->turn_rate); + JSON_ObjectAppendInt(lara_obj, "move_angle", lara->move_angle); + DUMP_XYZ(lara_obj, "head_rot", lara->head_rot); + DUMP_XYZ(lara_obj, "torso_rot", lara->torso_rot); + DUMP_XYZ(lara_obj, "last_pos", lara->last_pos); + + JSON_ObjectAppendObject(lara_obj, "left_arm", M_DumpArm(&lara->left_arm)); + JSON_ObjectAppendObject(lara_obj, "right_arm", M_DumpArm(&lara->right_arm)); + JSON_ObjectAppendObject( + lara_obj, "pistols", M_DumpAmmo(&lara->pistol_ammo)); + JSON_ObjectAppendObject( + lara_obj, "magnums", M_DumpAmmo(&lara->magnum_ammo)); + JSON_ObjectAppendObject(lara_obj, "uzis", M_DumpAmmo(&lara->uzi_ammo)); + JSON_ObjectAppendObject( + lara_obj, "shotgun", M_DumpAmmo(&lara->shotgun_ammo)); + JSON_ObjectAppendObject( + lara_obj, "harpoon", M_DumpAmmo(&lara->harpoon_ammo)); + JSON_ObjectAppendObject( + lara_obj, "grenade", M_DumpAmmo(&lara->grenade_ammo)); + JSON_ObjectAppendObject(lara_obj, "m16", M_DumpAmmo(&lara->m16_ammo)); + + if (lara->weapon_item != NO_ITEM) { + JSON_OBJECT *const weapon_obj = JSON_ObjectNew(); + const ITEM *const weapon_item = Item_Get(lara->weapon_item); + JSON_ObjectAppendInt(weapon_obj, "obj_id", weapon_item->object_id); + JSON_ObjectAppendInt(weapon_obj, "anim_num", weapon_item->anim_num); + JSON_ObjectAppendInt(weapon_obj, "frame_num", weapon_item->frame_num); + JSON_ObjectAppendInt( + weapon_obj, "current_anim_state", weapon_item->current_anim_state); + JSON_ObjectAppendInt( + weapon_obj, "goal_anim_state", weapon_item->goal_anim_state); + JSON_ObjectAppendObject(lara_obj, "weapon", weapon_obj); + } + + return lara_obj; +} + +static bool M_LoadLara(JSON_OBJECT *const lara_obj) +{ + if (lara_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing Lara info"); + return false; + } + + LARA_INFO *const lara = Lara_GetLaraInfo(); + ASSERT(lara != nullptr); + + lara->item_num = JSON_ObjectGetInt(lara_obj, "item_number", lara->item_num); + lara->gun_status = + JSON_ObjectGetInt(lara_obj, "gun_status", lara->gun_status); + lara->gun_type = JSON_ObjectGetInt(lara_obj, "gun_type", lara->gun_type); + lara->request_gun_type = + JSON_ObjectGetInt(lara_obj, "request_gun_type", lara->request_gun_type); + lara->last_gun_type = + JSON_ObjectGetInt(lara_obj, "last_gun_type", lara->last_gun_type); + lara->calc_fall_speed = + JSON_ObjectGetInt(lara_obj, "calc_fall_speed", lara->calc_fall_speed); + lara->water_status = + JSON_ObjectGetInt(lara_obj, "water_status", lara->water_status); + lara->climb_status = + JSON_ObjectGetInt(lara_obj, "climb_status", lara->climb_status); + lara->pose_count = + JSON_ObjectGetInt(lara_obj, "pose_count", lara->pose_count); + lara->hit_frame = JSON_ObjectGetInt(lara_obj, "hit_frame", lara->hit_frame); + lara->hit_direction = + JSON_ObjectGetInt(lara_obj, "hit_direction", lara->hit_direction); + lara->air = JSON_ObjectGetInt(lara_obj, "air", lara->air); + lara->dive_count = + JSON_ObjectGetInt(lara_obj, "dive_count", lara->dive_count); + lara->death_timer = + JSON_ObjectGetInt(lara_obj, "death_count", lara->death_timer); + lara->current_active = + JSON_ObjectGetInt(lara_obj, "current_active", lara->current_active); + lara->hit_effect_count = + JSON_ObjectGetInt(lara_obj, "hit_effect_count", lara->hit_effect_count); + lara->flare_age = JSON_ObjectGetInt(lara_obj, "flare_age", lara->flare_age); + lara->skidoo = + JSON_ObjectGetInt(lara_obj, "vehicle_item_number", lara->skidoo); + lara->back_gun = + JSON_ObjectGetInt(lara_obj, "back_gun_obj_id", lara->back_gun); + lara->flare_frame = + JSON_ObjectGetInt(lara_obj, "flare_frame", lara->flare_frame); + lara->mesh_effects = + JSON_ObjectGetInt(lara_obj, "mesh_effects", lara->mesh_effects); + lara->water_surface_dist = JSON_ObjectGetInt( + lara_obj, "water_surface_dist", lara->water_surface_dist); + + lara->flare_control_left = JSON_ObjectGetBool( + lara_obj, "flare_control_left", lara->flare_control_left); + lara->flare_control_right = JSON_ObjectGetBool( + lara_obj, "flare_control_right", lara->flare_control_right); + lara->extra_anim = + JSON_ObjectGetBool(lara_obj, "extra_anim", lara->extra_anim); + lara->look = JSON_ObjectGetBool(lara_obj, "look", lara->look); + lara->burn = JSON_ObjectGetBool(lara_obj, "burn", lara->burn); + + const JSON_ARRAY *const lara_meshes_arr = + JSON_ObjectGetArray(lara_obj, "meshes"); + if (!lara_meshes_arr) { + LOG_ERROR("Malformed save: invalid or missing Lara meshes"); + return false; + } + if ((signed)lara_meshes_arr->length != LM_NUMBER_OF) { + LOG_ERROR( + "Malformed save: expected %d Lara meshes, got %d", LM_NUMBER_OF, + lara_meshes_arr->length); + return false; + } + + for (int32_t i = 0; i < (signed)lara_meshes_arr->length; i++) { + int32_t idx = Object_GetMeshOffset(lara->mesh_ptrs[i]); + idx = JSON_ArrayGetInt(lara_meshes_arr, i, idx); + OBJECT_MESH *const mesh = Object_FindMesh(idx); + if (mesh != nullptr) { + lara->mesh_ptrs[i] = mesh; + } + } + + lara->target = nullptr; + lara->target_angles[0] = + JSON_ObjectGetInt(lara_obj, "target_angle1", lara->target_angles[0]); + lara->target_angles[1] = + JSON_ObjectGetInt(lara_obj, "target_angle2", lara->target_angles[1]); + lara->turn_rate = JSON_ObjectGetInt(lara_obj, "turn_rate", lara->turn_rate); + lara->move_angle = + JSON_ObjectGetInt(lara_obj, "move_angle", lara->move_angle); + LOAD_XYZ(lara_obj, "head_rot", lara->head_rot); + LOAD_XYZ(lara_obj, "torso_rot", lara->torso_rot); + LOAD_XYZ(lara_obj, "last_pos", lara->last_pos); + + if (!M_LoadArm( + JSON_ObjectGetObject(lara_obj, "left_arm"), &lara->left_arm)) { + return false; + } + + if (!M_LoadArm( + JSON_ObjectGetObject(lara_obj, "right_arm"), &lara->right_arm)) { + return false; + } + + if (!M_LoadAmmo( + JSON_ObjectGetObject(lara_obj, "pistols"), &lara->pistol_ammo)) { + return false; + } + + if (!M_LoadAmmo( + JSON_ObjectGetObject(lara_obj, "magnums"), &lara->magnum_ammo)) { + return false; + } + + if (!M_LoadAmmo(JSON_ObjectGetObject(lara_obj, "uzis"), &lara->uzi_ammo)) { + return false; + } + + if (!M_LoadAmmo( + JSON_ObjectGetObject(lara_obj, "shotgun"), &lara->shotgun_ammo)) { + return false; + } + + if (!M_LoadAmmo( + JSON_ObjectGetObject(lara_obj, "harpoon"), &lara->harpoon_ammo)) { + return false; + } + + if (!M_LoadAmmo( + JSON_ObjectGetObject(lara_obj, "grenade"), &lara->grenade_ammo)) { + return false; + } + + if (!M_LoadAmmo(JSON_ObjectGetObject(lara_obj, "m16"), &lara->m16_ammo)) { + return false; + } + + const JSON_OBJECT *const weapon_obj = + JSON_ObjectGetObject(lara_obj, "weapon"); + if (weapon_obj != nullptr) { + lara->weapon_item = Item_Create(); + ITEM *const weapon_item = Item_Get(lara->weapon_item); + weapon_item->object_id = + JSON_ObjectGetInt(weapon_obj, "obj_id", weapon_item->object_id); + weapon_item->anim_num = + JSON_ObjectGetInt(weapon_obj, "anim_num", weapon_item->anim_num); + weapon_item->frame_num = + JSON_ObjectGetInt(weapon_obj, "frame_num", weapon_item->frame_num); + weapon_item->current_anim_state = JSON_ObjectGetInt( + weapon_obj, "current_anim_state", weapon_item->current_anim_state); + weapon_item->goal_anim_state = JSON_ObjectGetInt( + weapon_obj, "goal_anim_state", weapon_item->goal_anim_state); + weapon_item->status = IS_ACTIVE; + weapon_item->room_num = NO_ROOM; + } + + return true; +} + +static JSON_OBJECT *M_DumpArm(const LARA_ARM *const arm) +{ + ASSERT(arm != nullptr); + JSON_OBJECT *const arm_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(arm_obj, "anim_num", arm->anim_num); + JSON_ObjectAppendInt(arm_obj, "frame_num", arm->frame_num); + JSON_ObjectAppendInt(arm_obj, "lock", arm->lock); + JSON_ObjectAppendInt(arm_obj, "flash_gun", arm->flash_gun); + DUMP_XYZ(arm_obj, "rot", arm->rot); + return arm_obj; +} + +static bool M_LoadArm(JSON_OBJECT *const arm_obj, LARA_ARM *const arm) +{ + ASSERT(arm != nullptr); + if (arm_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing arm info"); + return false; + } + + arm->frame_num = JSON_ObjectGetInt(arm_obj, "frame_num", arm->frame_num); + arm->lock = JSON_ObjectGetInt(arm_obj, "lock", arm->lock); + arm->flash_gun = JSON_ObjectGetInt(arm_obj, "flash_gun", arm->flash_gun); + LOAD_XYZ(arm_obj, "rot", arm->rot); + + return true; +} + +static JSON_OBJECT *M_DumpAmmo(const AMMO_INFO *const ammo) +{ + ASSERT(ammo != nullptr); + JSON_OBJECT *const ammo_obj = JSON_ObjectNew(); + JSON_ObjectAppendInt(ammo_obj, "ammo", ammo->ammo); + return ammo_obj; +} + +static bool M_LoadAmmo(JSON_OBJECT *const ammo_obj, AMMO_INFO *const ammo) +{ + ASSERT(ammo != nullptr); + if (ammo_obj == nullptr) { + LOG_ERROR("Malformed save: invalid or missing ammo info"); + return false; + } + + ammo->ammo = JSON_ObjectGetInt(ammo_obj, "ammo", ammo->ammo); + return true; +} + +REGISTER_SAVEGAME_STRATEGY(m_Strategy) diff --git a/src/tr2/decomp/savegame.c b/src/tr2/game/savegame/savegame_legacy.c similarity index 51% rename from src/tr2/decomp/savegame.c rename to src/tr2/game/savegame/savegame_legacy.c index 43cb4d425..862c1186e 100644 --- a/src/tr2/decomp/savegame.c +++ b/src/tr2/game/savegame/savegame_legacy.c @@ -1,5 +1,3 @@ -#include "decomp/savegame.h" - #include "game/camera.h" #include "game/game.h" #include "game/game_flow.h" @@ -9,21 +7,22 @@ #include "game/lara/misc.h" #include "game/lot.h" #include "game/objects/general/lift.h" -#include "game/requester.h" #include "game/room.h" +#include "game/savegame.h" #include "game/shell.h" #include "global/const.h" #include "global/vars.h" #include -#include #include -#include +#include #include #include #define SAVE_CREATURE (1 << 7) +#define SAVEGAME_LEGACY_TOTAL_SIZE (1170 + 6272) // header + OG buffer size +#define SAVEGAME_LEGACY_TITLE_SIZE 75 #define SPECIAL_READ_WRITES \ SPECIAL_READ_WRITE(S8, int8_t) \ @@ -33,21 +32,31 @@ SPECIAL_READ_WRITE(U16, uint16_t) \ SPECIAL_READ_WRITE(U32, uint32_t) +#pragma pack(push, 1) +typedef struct { + uint8_t num_pickup[2]; + uint8_t num_puzzle[4]; + uint8_t num_key[4]; + uint16_t reserved; +} SAVEGAME_LEGACY_ITEM_STATS; +#pragma pack(pop) + static int32_t m_BufPos = 0; static char *m_BufPtr = nullptr; -static uint32_t m_ReqFlags1[MAX_REQUESTER_ITEMS]; -static uint32_t m_ReqFlags2[MAX_REQUESTER_ITEMS]; -static void M_Reset(void); +static bool M_ItemHasSaveFlags(const OBJECT *obj, const ITEM *item); +static bool M_ItemHasSavePosition(const OBJECT *obj, const ITEM *item); + +static void M_Reset(char *buffer); static void M_Read(void *ptr, size_t size); #undef SPECIAL_READ_WRITE #define SPECIAL_READ_WRITE(name, type) static type M_Read##name(void); SPECIAL_READ_WRITES static void M_Skip(size_t size); -static void M_ReadStartInfo(MYFILE *fp, START_INFO *start); -static void M_ReadStartInfos(MYFILE *fp); -static void M_ReadStats(MYFILE *fp, LEVEL_STATS *const stats); +static void M_ReadResumeInfo(RESUME_INFO *resume); +static void M_ReadResumeInfos(void); +static void M_ReadStats(LEVEL_STATS *const stats); static void M_ReadItems(void); static void M_ReadLara(LARA_INFO *lara); static void M_ReadLaraArm(LARA_ARM *arm); @@ -58,24 +67,54 @@ static void M_Write(const void *ptr, size_t size); #undef SPECIAL_READ_WRITE #define SPECIAL_READ_WRITE(name, type) static void M_Write##name(type value); SPECIAL_READ_WRITES -static void M_WriteStartInfo(MYFILE *fp, const START_INFO *start); -static void M_WriteStartInfos(MYFILE *fp); -static void M_WriteStats(MYFILE *fp, const LEVEL_STATS *stats); +static void M_WriteResumeInfo(const RESUME_INFO *resume); +static void M_WriteResumeInfos(void); +static void M_WriteStats(const LEVEL_STATS *stats); static void M_WriteItems(void); static void M_WriteLara(const LARA_INFO *lara); static void M_WriteLaraArm(const LARA_ARM *arm); static void M_WriteAmmoInfo(const AMMO_INFO *ammo_info); static void M_WriteFlares(void); -static void M_Reset(void) +static const char *M_GetSaveFilePattern(void); +static bool M_FillInfo(MYFILE *fp, SAVEGAME_INFO *info); +static void M_SaveToFile(MYFILE *fp, SAVEGAME_INFO *info); +static bool M_LoadFromFile(MYFILE *fp); + +static SAVEGAME_STRATEGY m_Strategy = { + // clang-format off + .allow_load = true, + .allow_save = false, + .format = SAVEGAME_FORMAT_LEGACY, + .get_save_file_pattern_func = M_GetSaveFilePattern, + .fill_info_func = M_FillInfo, + .load_from_file_func = M_LoadFromFile, + .save_to_file_func = M_SaveToFile, + .load_only_resume_info_func = nullptr, + .update_death_counters_func = nullptr, + // clang-format on +}; + +static bool M_ItemHasSaveFlags(const OBJECT *const obj, const ITEM *const item) +{ + return obj->save_flags && item->object_id != O_WATERFALL; +} + +static bool M_ItemHasSavePosition( + const OBJECT *const obj, const ITEM *const item) +{ + return obj->save_position && item->object_id != O_GONDOLA; +} + +static void M_Reset(char *const buffer) { m_BufPos = 0; - m_BufPtr = g_SaveGame.buffer; + m_BufPtr = buffer; } static void M_Read(void *const ptr, const size_t size) { - ASSERT(m_BufPos + size <= MAX_SG_BUFFER_SIZE); + ASSERT(m_BufPos + size <= SAVEGAME_LEGACY_TOTAL_SIZE); m_BufPos += size; memcpy(ptr, m_BufPtr, size); m_BufPtr += size; @@ -105,74 +144,72 @@ static void M_Skip(const size_t size) m_BufPtr += size; } -static void M_ReadStartInfo(MYFILE *const fp, START_INFO *const start) +static void M_ReadResumeInfo(RESUME_INFO *const resume) { - start->pistol_ammo = File_ReadU16(fp); - start->magnum_ammo = File_ReadU16(fp); - start->uzi_ammo = File_ReadU16(fp); - start->shotgun_ammo = File_ReadU16(fp); - start->m16_ammo = File_ReadU16(fp); - start->grenade_ammo = File_ReadU16(fp); - start->harpoon_ammo = File_ReadU16(fp); - start->small_medipacks = File_ReadU8(fp); - start->large_medipacks = File_ReadU8(fp); - start->reserved1 = File_ReadU8(fp); - start->flares = File_ReadU8(fp); - start->gun_status = File_ReadU8(fp); - start->gun_type = File_ReadU8(fp); + resume->pistol_ammo = M_ReadU16(); + resume->magnum_ammo = M_ReadU16(); + resume->uzi_ammo = M_ReadU16(); + resume->shotgun_ammo = M_ReadU16(); + resume->m16_ammo = M_ReadU16(); + resume->grenade_ammo = M_ReadU16(); + resume->harpoon_ammo = M_ReadU16(); + resume->small_medipacks = M_ReadU8(); + resume->large_medipacks = M_ReadU8(); + M_Skip(sizeof(uint8_t)); // legacy reserved value + resume->flares = M_ReadU8(); + resume->gun_status = M_ReadU8(); + resume->equipped_gun_type = M_ReadU8(); - const uint16_t flags = File_ReadU16(fp); + const uint16_t flags = M_ReadU16(); // clang-format off - start->available = (flags & 0x01) ? 1 : 0; - start->has_pistols = (flags & 0x02) ? 1 : 0; - start->has_magnums = (flags & 0x04) ? 1 : 0; - start->has_uzis = (flags & 0x08) ? 1 : 0; - start->has_shotgun = (flags & 0x10) ? 1 : 0; - start->has_m16 = (flags & 0x20) ? 1 : 0; - start->has_grenade = (flags & 0x40) ? 1 : 0; - start->has_harpoon = (flags & 0x80) ? 1 : 0; + resume->flags.available = (flags & 0x01) ? 1 : 0; + resume->flags.has_pistols = (flags & 0x02) ? 1 : 0; + resume->flags.has_magnums = (flags & 0x04) ? 1 : 0; + resume->flags.has_uzis = (flags & 0x08) ? 1 : 0; + resume->flags.has_shotgun = (flags & 0x10) ? 1 : 0; + resume->flags.has_m16 = (flags & 0x20) ? 1 : 0; + resume->flags.has_grenade = (flags & 0x40) ? 1 : 0; + resume->flags.has_harpoon = (flags & 0x80) ? 1 : 0; // clang-format on - File_ReadU16(fp); - M_ReadStats(fp, &start->stats); + M_Skip(sizeof(uint16_t)); + M_ReadStats(&resume->stats); } -static void M_ReadStartInfos(MYFILE *const fp) +static void M_ReadResumeInfos(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < 24; i++) { if (i < level_table->count) { const GF_LEVEL *const level = &level_table->levels[i]; - M_ReadStartInfo(fp, Savegame_GetCurrentInfo(level)); + M_ReadResumeInfo(Savegame_GetCurrentInfo(level)); } else { - START_INFO dummy_resume_info; - M_ReadStartInfo(fp, &dummy_resume_info); + RESUME_INFO dummy_resume_info; + M_ReadResumeInfo(&dummy_resume_info); } } } -static void M_ReadStats(MYFILE *const fp, LEVEL_STATS *const stats) +static void M_ReadStats(LEVEL_STATS *const stats) { - stats->timer = File_ReadU32(fp); - stats->ammo_used = File_ReadU32(fp); - stats->ammo_hits = File_ReadU32(fp); - stats->distance = File_ReadU32(fp); - stats->kills = File_ReadU16(fp); - stats->secret_flags = File_ReadU8(fp); - stats->medipacks = File_ReadU8(fp); + stats->timer = M_ReadU32(); + stats->ammo_used = M_ReadU32(); + stats->ammo_hits = M_ReadU32(); + stats->distance_travelled = M_ReadU32(); + stats->kill_count = M_ReadU16(); + stats->secret_flags = M_ReadU8(); + stats->medipacks_used = M_ReadU8() / 2.0f; } static void M_ReadItems(void) { + Savegame_ProcessItemsBeforeLoad(); + for (int32_t item_num = 0; item_num < Item_GetLevelCount(); item_num++) { ITEM *const item = Item_Get(item_num); const OBJECT *const obj = Object_Get(item->object_id); - if (obj->handle_save_func != nullptr) { - obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD); - } - - if (obj->save_position) { + if (M_ItemHasSavePosition(obj, item)) { item->pos.x = M_ReadS32(); item->pos.y = M_ReadS32(); item->pos.z = M_ReadS32(); @@ -186,13 +223,6 @@ static void M_ReadItems(void) if (item->room_num != room_num) { Item_NewRoom(item_num, room_num); } - - if (obj->shadow_size != 0) { - const SECTOR *const sector = Room_GetSector( - item->pos.x, item->pos.y, item->pos.z, &room_num); - item->floor = Room_GetHeight( - sector, item->pos.x, item->pos.y, item->pos.z); - } } if (obj->save_anim) { @@ -207,7 +237,7 @@ static void M_ReadItems(void) item->hit_points = M_ReadS16(); } - if (obj->save_flags) { + if (M_ItemHasSaveFlags(obj, item)) { item->flags = M_ReadU16(); if (obj->intelligent) { @@ -252,8 +282,6 @@ static void M_ReadItems(void) Item_SetPrevActive(item_num); } } - - item->flags &= 0xFF00; } switch (item->object_id) { @@ -274,8 +302,6 @@ static void M_ReadItems(void) obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD); } } - - MovableBlock_SetupFloor(); } static void M_ReadLara(LARA_INFO *const lara) @@ -396,7 +422,7 @@ static void M_ReadFlares(void) static void M_Write(const void *ptr, const size_t size) { m_BufPos += size; - if (m_BufPos >= MAX_SG_BUFFER_SIZE) { + if (m_BufPos >= SAVEGAME_LEGACY_TOTAL_SIZE) { Shell_ExitSystem("Savegame is too big to fit in buffer"); } @@ -404,71 +430,73 @@ static void M_Write(const void *ptr, const size_t size) m_BufPtr += size; } -static void M_WriteStartInfo(MYFILE *const fp, const START_INFO *const start) +static void M_WriteResumeInfo(const RESUME_INFO *const resume) { - ASSERT(start != nullptr); - File_WriteU16(fp, start->pistol_ammo); - File_WriteU16(fp, start->magnum_ammo); - File_WriteU16(fp, start->uzi_ammo); - File_WriteU16(fp, start->shotgun_ammo); - File_WriteU16(fp, start->m16_ammo); - File_WriteU16(fp, start->grenade_ammo); - File_WriteU16(fp, start->harpoon_ammo); - File_WriteU8(fp, start->small_medipacks); - File_WriteU8(fp, start->large_medipacks); - File_WriteU8(fp, start->reserved1); - File_WriteU8(fp, start->flares); - File_WriteU8(fp, start->gun_status); - File_WriteU8(fp, start->gun_type); + ASSERT(resume != nullptr); + M_WriteU16(resume->pistol_ammo); + M_WriteU16(resume->magnum_ammo); + M_WriteU16(resume->uzi_ammo); + M_WriteU16(resume->shotgun_ammo); + M_WriteU16(resume->m16_ammo); + M_WriteU16(resume->grenade_ammo); + M_WriteU16(resume->harpoon_ammo); + M_WriteU8(resume->small_medipacks); + M_WriteU8(resume->large_medipacks); + M_WriteU8(0); // legacy reserved value + M_WriteU8(resume->flares); + M_WriteU8(resume->gun_status); + M_WriteU8(resume->equipped_gun_type); uint16_t flags = 0; // clang-format off - if (start->available) { flags |= 0x01; } - if (start->has_pistols) { flags |= 0x02; } - if (start->has_magnums) { flags |= 0x04; } - if (start->has_uzis) { flags |= 0x08; } - if (start->has_shotgun) { flags |= 0x10; } - if (start->has_m16) { flags |= 0x20; } - if (start->has_grenade) { flags |= 0x40; } - if (start->has_harpoon) { flags |= 0x80; } + if (resume->flags.available) { flags |= 0x01; } + if (resume->flags.has_pistols) { flags |= 0x02; } + if (resume->flags.has_magnums) { flags |= 0x04; } + if (resume->flags.has_uzis) { flags |= 0x08; } + if (resume->flags.has_shotgun) { flags |= 0x10; } + if (resume->flags.has_m16) { flags |= 0x20; } + if (resume->flags.has_grenade) { flags |= 0x40; } + if (resume->flags.has_harpoon) { flags |= 0x80; } // clang-format on - File_WriteU16(fp, flags); + M_WriteU16(flags); - File_WriteU16(fp, 0); - M_WriteStats(fp, &start->stats); + M_WriteU16(0); + M_WriteStats(&resume->stats); } -static void M_WriteStartInfos(MYFILE *const fp) +static void M_WriteResumeInfos(void) { const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); for (int32_t i = 0; i < 24; i++) { if (i < level_table->count) { const GF_LEVEL *const level = &level_table->levels[i]; - M_WriteStartInfo(fp, Savegame_GetCurrentInfo(level)); + M_WriteResumeInfo(Savegame_GetCurrentInfo(level)); } else { - const START_INFO null_resume_info = {}; - M_WriteStartInfo(fp, &null_resume_info); + const RESUME_INFO null_resume_info = {}; + M_WriteResumeInfo(&null_resume_info); } } } -static void M_WriteStats(MYFILE *const fp, const LEVEL_STATS *const stats) +static void M_WriteStats(const LEVEL_STATS *const stats) { - File_WriteU32(fp, stats->timer); - File_WriteU32(fp, stats->ammo_used); - File_WriteU32(fp, stats->ammo_hits); - File_WriteU32(fp, stats->distance); - File_WriteU16(fp, stats->kills); - File_WriteU8(fp, stats->secret_flags); - File_WriteU8(fp, stats->medipacks); + M_WriteU32(stats->timer); + M_WriteU32(stats->ammo_used); + M_WriteU32(stats->ammo_hits); + M_WriteU32(stats->distance_travelled); + M_WriteU16(stats->kill_count); + M_WriteU8(stats->secret_flags & 0xFF); + M_WriteU8(stats->medipacks_used * 2); } static void M_WriteItems(void) { + Savegame_ProcessItemsBeforeSave(); + for (int32_t i = 0; i < Item_GetLevelCount(); i++) { const ITEM *const item = Item_Get(i); const OBJECT *const obj = Object_Get(item->object_id); - if (obj->save_position) { + if (M_ItemHasSavePosition(obj, item)) { M_WriteS32(item->pos.x); M_WriteS32(item->pos.y); M_WriteS32(item->pos.z); @@ -492,7 +520,7 @@ static void M_WriteItems(void) M_WriteS16(item->hit_points); } - if (obj->save_flags) { + if (M_ItemHasSaveFlags(obj, item)) { uint16_t flags = item->flags + item->active + (item->status << 1) + (item->gravity << 3) + (item->collidable << 4); if (obj->intelligent && item->data != nullptr) { @@ -648,255 +676,92 @@ static void M_WriteFlares(void) } } -void Savegame_ResetCurrentInfo(const GF_LEVEL *const level) +static const char *M_GetSaveFilePattern(void) { - START_INFO *const current = Savegame_GetCurrentInfo(level); - memset(current, 0, sizeof(START_INFO)); + return g_GameFlow.savegame_fmt_legacy; } -void Savegame_InitCurrentInfo(void) +static bool M_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const savegame_info) { - if (g_SaveGame.bonus_flag) { - return; + char level_title[75]; + File_ReadData(fp, level_title, 75); + savegame_info->level_title = Memory_DupStr(level_title); + savegame_info->counter = File_ReadS32(fp); + + for (int32_t i = 0; i < 24; i++) { + File_Skip(fp, sizeof(uint16_t)); // pistol ammo + File_Skip(fp, sizeof(uint16_t)); // magnum ammo + File_Skip(fp, sizeof(uint16_t)); // uzi ammo + File_Skip(fp, sizeof(uint16_t)); // shotgun ammo + File_Skip(fp, sizeof(uint16_t)); // m16 ammo + File_Skip(fp, sizeof(uint16_t)); // grenade ammo + File_Skip(fp, sizeof(uint16_t)); // harpoon ammo + File_Skip(fp, sizeof(uint8_t)); // small medis + File_Skip(fp, sizeof(uint8_t)); // big medis + File_Skip(fp, sizeof(uint8_t)); // reserved + File_Skip(fp, sizeof(uint8_t)); // flares + File_Skip(fp, sizeof(int8_t)); // gun status + File_Skip(fp, sizeof(int8_t)); // gun type + File_Skip(fp, sizeof(uint16_t)); // flags + File_Skip(fp, sizeof(uint16_t)); // unused + File_Skip(fp, sizeof(uint32_t)); // timer + File_Skip(fp, sizeof(uint32_t)); // ammo used + File_Skip(fp, sizeof(uint32_t)); // hits + File_Skip(fp, sizeof(uint32_t)); // distance + File_Skip(fp, sizeof(uint16_t)); // kills + File_Skip(fp, sizeof(uint8_t)); // secret flags + File_Skip(fp, sizeof(uint8_t)); // medis used } - const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN); - for (int32_t i = 0; i < level_table->count; i++) { - const GF_LEVEL *const level = &level_table->levels[i]; - Savegame_ResetCurrentInfo(level); - Savegame_ApplyLogicToCurrentInfo(level); - Savegame_GetCurrentInfo(level)->available = 0; - } + File_Skip(fp, sizeof(uint32_t)); // timer + File_Skip(fp, sizeof(uint32_t)); // ammo used + File_Skip(fp, sizeof(uint32_t)); // hits + File_Skip(fp, sizeof(uint32_t)); // distance + File_Skip(fp, sizeof(uint16_t)); // kills + File_Skip(fp, sizeof(uint8_t)); // secret flags + File_Skip(fp, sizeof(uint8_t)); // medis used - if (GF_GetGymLevel() != nullptr) { - Savegame_GetCurrentInfo(GF_GetGymLevel())->available = 1; - } - if (GF_GetFirstLevel() != nullptr) { - Savegame_GetCurrentInfo(GF_GetFirstLevel())->available = 1; - } - g_SaveGame.bonus_flag = 0; + savegame_info->level_num = File_ReadS16(fp); + savegame_info->initial_version = VERSION_LEGACY; + savegame_info->features.restart = false; + savegame_info->features.select_level = false; + + return true; } -void Savegame_CarryCurrentInfoToNextLevel( - const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level) +static void M_SaveToFile(MYFILE *const fp, SAVEGAME_INFO *const info) { - LOG_INFO( - "Copying resume info from level #%d to level #%d", src_level->num, - dst_level->num); - START_INFO *const src_resume = Savegame_GetCurrentInfo(src_level); - START_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level); - memcpy(dst_resume, src_resume, sizeof(START_INFO)); -} + char *buffer = Memory_Alloc(SAVEGAME_LEGACY_TOTAL_SIZE); + M_Reset(buffer); -void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level) -{ - START_INFO *start = Savegame_GetCurrentInfo(level); - if (start == nullptr) { - return; - } + const GF_LEVEL *const current_level = GF_GetCurrentLevel(); + const RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(current_level); - start->has_pistols = 1; - start->gun_type = LGT_PISTOLS; - start->pistol_ammo = 1000; + char title[SAVEGAME_LEGACY_TITLE_SIZE]; + snprintf(title, SAVEGAME_LEGACY_TITLE_SIZE, "%s", current_level->title); + M_Write(title, SAVEGAME_LEGACY_TITLE_SIZE); + M_WriteS32(Savegame_GetCounter()); - if (level == GF_GetGymLevel()) { - start->available = 1; + M_WriteResumeInfos(); + M_WriteStats(¤t_info->stats); + M_WriteS16(current_level->num); + M_WriteU8(Game_IsBonusFlagSet(GBF_NGPLUS) ? 1 : 0); - start->has_pistols = 0; - start->has_shotgun = 0; - start->has_magnums = 0; - start->has_uzis = 0; - start->has_harpoon = 0; - start->has_m16 = 0; - start->has_grenade = 0; - - start->pistol_ammo = 0; - start->shotgun_ammo = 0; - start->magnum_ammo = 0; - start->uzi_ammo = 0; - start->harpoon_ammo = 0; - start->m16_ammo = 0; - start->grenade_ammo = 0; - - start->flares = 0; - start->large_medipacks = 0; - start->small_medipacks = 0; - start->gun_type = LGT_UNARMED; - start->gun_status = LGS_ARMLESS; - } else if (level == GF_GetFirstLevel()) { - start->available = 1; - - start->has_pistols = 1; - start->has_shotgun = 1; - start->has_magnums = 0; - start->has_uzis = 0; - start->has_harpoon = 0; - start->has_m16 = 0; - start->has_grenade = 0; - - start->shotgun_ammo = 2 * SHOTGUN_AMMO_CLIP; - start->magnum_ammo = 0; - start->uzi_ammo = 0; - start->harpoon_ammo = 0; - start->m16_ammo = 0; - start->grenade_ammo = 0; - - start->flares = 2; - start->small_medipacks = 1; - start->large_medipacks = 1; - start->gun_status = LGS_ARMLESS; - } - - if (g_SaveGame.bonus_flag && level != GF_GetGymLevel()) { - start->has_pistols = 1; - start->has_shotgun = 1; - start->has_magnums = 1; - start->has_uzis = 1; - start->has_grenade = 1; - start->has_harpoon = 1; - start->has_m16 = 1; - start->has_grenade = 1; - - start->shotgun_ammo = 10000; - start->magnum_ammo = 10000; - start->uzi_ammo = 10000; - start->harpoon_ammo = 10000; - start->m16_ammo = 10000; - start->grenade_ammo = 10000; - - start->flares = -1; - start->gun_type = LGT_GRENADE; - } - - if (g_GF_RemoveWeapons) { - start->has_pistols = 0; - start->has_magnums = 0; - start->has_uzis = 0; - start->has_shotgun = 0; - start->has_m16 = 0; - start->has_grenade = 0; - start->has_harpoon = 0; - start->gun_type = LGT_UNARMED; - start->gun_status = LGS_ARMLESS; - g_GF_RemoveWeapons = false; - } - - if (g_GF_RemoveAmmo) { - start->m16_ammo = 0; - start->grenade_ammo = 0; - start->harpoon_ammo = 0; - start->shotgun_ammo = 0; - start->uzi_ammo = 0; - start->magnum_ammo = 0; - start->pistol_ammo = 0; - start->flares = 0; - start->large_medipacks = 0; - start->small_medipacks = 0; - g_GF_RemoveAmmo = false; - } - - const STATS_COMMON default_stats = Savegame_GetDefaultStats(level); - start->stats.max_secret_count = default_stats.max_secret_count; -} - -void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level) -{ - START_INFO *const start = Savegame_GetCurrentInfo(level); - - start->available = 1; - - if (Inv_RequestItem(O_PISTOL_ITEM)) { - start->has_pistols = 1; - start->pistol_ammo = 1000; - } else { - start->has_pistols = 0; - start->pistol_ammo = 1000; - } - - if (Inv_RequestItem(O_SHOTGUN_ITEM)) { - start->has_shotgun = 1; - start->shotgun_ammo = g_Lara.shotgun_ammo.ammo; - } else { - start->has_shotgun = 0; - start->shotgun_ammo = - Inv_RequestItem(O_SHOTGUN_AMMO_ITEM) * SHOTGUN_AMMO_QTY; - } - - if (Inv_RequestItem(O_MAGNUM_ITEM)) { - start->has_magnums = 1; - start->magnum_ammo = g_Lara.magnum_ammo.ammo; - } else { - start->has_magnums = 0; - start->magnum_ammo = - Inv_RequestItem(O_MAGNUM_AMMO_ITEM) * MAGNUM_AMMO_QTY; - } - - if (Inv_RequestItem(O_UZI_ITEM)) { - start->has_uzis = 1; - start->uzi_ammo = g_Lara.uzi_ammo.ammo; - } else { - start->has_uzis = 0; - start->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * UZI_AMMO_QTY; - } - - if (Inv_RequestItem(O_M16_ITEM)) { - start->has_m16 = 1; - start->m16_ammo = g_Lara.m16_ammo.ammo; - } else { - start->has_m16 = 0; - start->m16_ammo = Inv_RequestItem(O_M16_AMMO_ITEM) * M16_AMMO_QTY; - } - - if (Inv_RequestItem(O_HARPOON_ITEM)) { - start->has_harpoon = 1; - start->harpoon_ammo = g_Lara.harpoon_ammo.ammo; - } else { - start->has_harpoon = 0; - start->harpoon_ammo = - Inv_RequestItem(O_HARPOON_AMMO_ITEM) * HARPOON_AMMO_QTY; - } - - if (Inv_RequestItem(O_GRENADE_ITEM)) { - start->has_grenade = 1; - start->grenade_ammo = g_Lara.grenade_ammo.ammo; - } else { - start->has_grenade = 0; - start->grenade_ammo = - Inv_RequestItem(O_GRENADE_AMMO_ITEM) * GRENADE_AMMO_QTY; - } - - start->flares = Inv_RequestItem(O_FLARE_ITEM); - start->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM); - start->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM); - - if (g_Lara.gun_type == LGT_FLARE) { - start->gun_type = g_Lara.last_gun_type; - } else { - start->gun_type = g_Lara.gun_type; - } - start->gun_status = LGS_ARMLESS; -} - -void CreateSaveGameInfo(void) -{ - const GF_LEVEL *const current_level = Game_GetCurrentLevel(); - g_SaveGame.current_level = current_level->num; - Savegame_PersistGameToCurrentInfo(current_level); - - // TODO: refactor me! - g_SaveGame.num_pickup[0] = Inv_RequestItem(O_PICKUP_ITEM_1); - g_SaveGame.num_pickup[1] = Inv_RequestItem(O_PICKUP_ITEM_2); - g_SaveGame.num_puzzle[0] = Inv_RequestItem(O_PUZZLE_ITEM_1); - g_SaveGame.num_puzzle[1] = Inv_RequestItem(O_PUZZLE_ITEM_2); - g_SaveGame.num_puzzle[2] = Inv_RequestItem(O_PUZZLE_ITEM_3); - g_SaveGame.num_puzzle[3] = Inv_RequestItem(O_PUZZLE_ITEM_4); - g_SaveGame.num_key[0] = Inv_RequestItem(O_KEY_ITEM_1); - g_SaveGame.num_key[1] = Inv_RequestItem(O_KEY_ITEM_2); - g_SaveGame.num_key[2] = Inv_RequestItem(O_KEY_ITEM_3); - g_SaveGame.num_key[3] = Inv_RequestItem(O_KEY_ITEM_4); - - M_Reset(); - memset(g_SaveGame.buffer, 0, MAX_SG_BUFFER_SIZE); + const SAVEGAME_LEGACY_ITEM_STATS item_stats = { + .num_pickup[0] = Inv_RequestItem(O_PICKUP_ITEM_1), + .num_pickup[1] = Inv_RequestItem(O_PICKUP_ITEM_2), + .num_puzzle[0] = Inv_RequestItem(O_PUZZLE_ITEM_1), + .num_puzzle[1] = Inv_RequestItem(O_PUZZLE_ITEM_2), + .num_puzzle[2] = Inv_RequestItem(O_PUZZLE_ITEM_3), + .num_puzzle[3] = Inv_RequestItem(O_PUZZLE_ITEM_4), + .num_key[0] = Inv_RequestItem(O_KEY_ITEM_1), + .num_key[1] = Inv_RequestItem(O_KEY_ITEM_2), + .num_key[2] = Inv_RequestItem(O_KEY_ITEM_3), + .num_key[3] = Inv_RequestItem(O_KEY_ITEM_4), + .reserved = 0, + }; + M_Write(&item_stats, sizeof(SAVEGAME_LEGACY_ITEM_STATS)); M_WriteS32(Room_GetFlipStatus()); for (int32_t i = 0; i < MAX_FLIP_MAPS; i++) { @@ -926,27 +791,54 @@ void CreateSaveGameInfo(void) M_WriteS32(Room_GetFlipEffect()); M_WriteS32(Room_GetFlipTimer()); - M_WriteS32(g_IsMonkAngry); + M_WriteS32(Creature_AreAlliesHostile()); M_WriteFlares(); + + File_WriteData(fp, buffer, SAVEGAME_LEGACY_TOTAL_SIZE); + Memory_FreePointer(&buffer); } -void ExtractSaveGameInfo(void) +static bool M_LoadFromFile(MYFILE *const fp) { + char *buffer = Memory_Alloc(File_Size(fp)); + File_Seek(fp, 0, FILE_SEEK_SET); + File_ReadData(fp, buffer, File_Size(fp)); + + M_Reset(buffer); + M_Skip(SAVEGAME_LEGACY_TITLE_SIZE); + M_Skip(sizeof(int32_t)); // save counter + + M_ReadResumeInfos(); + { + LEVEL_STATS current_stats = {}; + M_ReadStats(¤t_stats); + const int16_t current_level = M_ReadS16(); + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, current_level); + RESUME_INFO *const current_info = Savegame_GetCurrentInfo(level); + current_info->stats = current_stats; + } + + const bool is_ng_plus = M_ReadU8() != 0; + if (is_ng_plus) { + Game_SetBonusFlag(GBF_NGPLUS); + } + + SAVEGAME_LEGACY_ITEM_STATS item_stats = {}; + M_Read(&item_stats, sizeof(SAVEGAME_LEGACY_ITEM_STATS)); + const GF_LEVEL *const current_level = Game_GetCurrentLevel(); Lara_InitialiseInventory(current_level); - Inv_AddItemNTimes(O_PICKUP_ITEM_1, g_SaveGame.num_pickup[0]); - Inv_AddItemNTimes(O_PICKUP_ITEM_2, g_SaveGame.num_pickup[1]); - Inv_AddItemNTimes(O_PUZZLE_ITEM_1, g_SaveGame.num_puzzle[0]); - Inv_AddItemNTimes(O_PUZZLE_ITEM_2, g_SaveGame.num_puzzle[1]); - Inv_AddItemNTimes(O_PUZZLE_ITEM_3, g_SaveGame.num_puzzle[2]); - Inv_AddItemNTimes(O_PUZZLE_ITEM_4, g_SaveGame.num_puzzle[3]); - Inv_AddItemNTimes(O_KEY_ITEM_1, g_SaveGame.num_key[0]); - Inv_AddItemNTimes(O_KEY_ITEM_2, g_SaveGame.num_key[1]); - Inv_AddItemNTimes(O_KEY_ITEM_3, g_SaveGame.num_key[2]); - Inv_AddItemNTimes(O_KEY_ITEM_4, g_SaveGame.num_key[3]); - - M_Reset(); + Inv_AddItemNTimes(O_PICKUP_ITEM_1, item_stats.num_pickup[0]); + Inv_AddItemNTimes(O_PICKUP_ITEM_2, item_stats.num_pickup[1]); + Inv_AddItemNTimes(O_PUZZLE_ITEM_1, item_stats.num_puzzle[0]); + Inv_AddItemNTimes(O_PUZZLE_ITEM_2, item_stats.num_puzzle[1]); + Inv_AddItemNTimes(O_PUZZLE_ITEM_3, item_stats.num_puzzle[2]); + Inv_AddItemNTimes(O_PUZZLE_ITEM_4, item_stats.num_puzzle[3]); + Inv_AddItemNTimes(O_KEY_ITEM_1, item_stats.num_key[0]); + Inv_AddItemNTimes(O_KEY_ITEM_2, item_stats.num_key[1]); + Inv_AddItemNTimes(O_KEY_ITEM_3, item_stats.num_key[2]); + Inv_AddItemNTimes(O_KEY_ITEM_4, item_stats.num_key[3]); if (M_ReadS32()) { Room_FlipMap(); @@ -981,145 +873,14 @@ void ExtractSaveGameInfo(void) weapon_item->room_num = NO_ROOM; } - if (g_Lara.burn) { - g_Lara.burn = 0; - Lara_CatchFire(); - } - Room_SetFlipEffect(M_ReadS32()); Room_SetFlipTimer(M_ReadS32()); - g_IsMonkAngry = M_ReadS32(); + Creature_SetAlliesHostile(M_ReadS32() != 0); M_ReadFlares(); -} -void GetSavedGamesList(REQUEST_INFO *const req) -{ - Requester_SetSize(req, 10, -32); - if (req->selected >= req->visible_count) { - req->line_offset = req->selected - req->visible_count + 1; - } - memcpy(g_RequesterFlags1, m_ReqFlags1, sizeof(m_ReqFlags1)); - memcpy(g_RequesterFlags2, m_ReqFlags2, sizeof(m_ReqFlags2)); -} - -bool S_FrontEndCheck(void) -{ - Requester_Init(&g_LoadGameRequester); - - g_SavedGames = 0; - for (int32_t i = 0; i < MAX_REQUESTER_ITEMS; i++) { - char file_name[80]; - sprintf(file_name, g_GameFlow.savegame_fmt_legacy, i); - - if (!File_Exists(file_name)) { - Requester_AddItem( - &g_LoadGameRequester, GS(MISC_EMPTY_SLOT), 0, 0, 0); - g_SavedLevels[i] = false; - } else { - MYFILE *const fp = File_Open(file_name, FILE_OPEN_READ); - char level_title[80]; - File_ReadData(fp, level_title, 75); - const int32_t save_num = File_ReadS32(fp); - File_Close(fp); - - char save_num_text[16]; - sprintf(save_num_text, "%d", save_num); - - Requester_AddItem( - &g_LoadGameRequester, level_title, REQ_ALIGN_LEFT, - save_num_text, REQ_ALIGN_RIGHT); - - if (save_num > g_SaveCounter) { - g_SaveCounter = save_num; - g_LoadGameRequester.selected = i; - } - g_SavedLevels[i] = true; - g_SavedGames++; - } - } - - memcpy(m_ReqFlags1, g_RequesterFlags1, sizeof(m_ReqFlags1)); - memcpy(m_ReqFlags2, g_RequesterFlags2, sizeof(m_ReqFlags2)); - g_SaveCounter++; + Memory_FreePointer(&buffer); return true; } -bool S_SaveGame(const int32_t slot_num) -{ - char file_name[80]; - sprintf(file_name, g_GameFlow.savegame_fmt_legacy, slot_num); - - MYFILE *const fp = File_Open(file_name, FILE_OPEN_WRITE); - if (fp == nullptr) { - return false; - } - - const GF_LEVEL *const current_level = - GF_GetLevel(GFLT_MAIN, g_SaveGame.current_level); - - memset(file_name, 0, 75); - snprintf(file_name, 75, "%s", current_level->title); - File_WriteData(fp, file_name, 75); - File_WriteS32(fp, g_SaveCounter); - M_WriteStartInfos(fp); - M_WriteStats(fp, &g_SaveGame.current_stats); - File_WriteS16(fp, g_SaveGame.current_level); - File_WriteU8(fp, g_SaveGame.bonus_flag); - for (int32_t i = 0; i < 2; i++) { - File_WriteU8(fp, g_SaveGame.num_pickup[i]); - } - for (int32_t i = 0; i < 4; i++) { - File_WriteU8(fp, g_SaveGame.num_puzzle[i]); - } - for (int32_t i = 0; i < 4; i++) { - File_WriteU8(fp, g_SaveGame.num_key[i]); - } - File_WriteS16(fp, 0); - File_WriteData(fp, g_SaveGame.buffer, MAX_SG_BUFFER_SIZE); - File_Close(fp); - - char save_num_text[16]; - sprintf(save_num_text, "%d", g_SaveCounter); - Requester_ChangeItem( - &g_LoadGameRequester, slot_num, file_name, REQ_ALIGN_LEFT, - save_num_text, REQ_ALIGN_RIGHT); - - m_ReqFlags1[slot_num] = g_RequesterFlags1[slot_num]; - m_ReqFlags2[slot_num] = g_RequesterFlags2[slot_num]; - g_SavedLevels[slot_num] = true; - g_SaveCounter++; - g_SavedGames++; - - return true; -} - -bool S_LoadGame(const int32_t slot_num) -{ - char file_name[80]; - sprintf(file_name, g_GameFlow.savegame_fmt_legacy, slot_num); - - MYFILE *const fp = File_Open(file_name, FILE_OPEN_READ); - if (fp == nullptr) { - return false; - } - File_Skip(fp, 75); - File_Skip(fp, 4); - M_ReadStartInfos(fp); - M_ReadStats(fp, &g_SaveGame.current_stats); - g_SaveGame.current_level = File_ReadS16(fp); - g_SaveGame.bonus_flag = File_ReadU8(fp); - for (int32_t i = 0; i < 2; i++) { - g_SaveGame.num_pickup[i] = File_ReadU8(fp); - } - for (int32_t i = 0; i < 4; i++) { - g_SaveGame.num_puzzle[i] = File_ReadU8(fp); - } - for (int32_t i = 0; i < 4; i++) { - g_SaveGame.num_key[i] = File_ReadU8(fp); - } - File_ReadS16(fp); - File_ReadData(fp, g_SaveGame.buffer, MAX_SG_BUFFER_SIZE); - File_Close(fp); - return true; -} +REGISTER_SAVEGAME_STRATEGY(m_Strategy) diff --git a/src/tr2/game/shell/common.c b/src/tr2/game/shell/common.c index f314418db..c3603f996 100644 --- a/src/tr2/game/shell/common.c +++ b/src/tr2/game/shell/common.c @@ -1,7 +1,6 @@ #include "game/shell/common.h" #include "decomp/decomp.h" -#include "decomp/savegame.h" #include "game/clock.h" #include "game/console/common.h" #include "game/demo.h" @@ -19,6 +18,7 @@ #include "game/phase.h" #include "game/random.h" #include "game/render/common.h" +#include "game/savegame.h" #include "game/sound.h" #include "game/text.h" #include "game/viewport.h" @@ -32,7 +32,7 @@ #include #include #include -#include +#include #include #include @@ -77,10 +77,13 @@ static SHELL_ARGS m_Args = { .save_to_load = -1, }; +static SHELL_SIZE m_ViewportSize = { .w = -1, .h = -1 }; static Uint64 m_UpdateDebounce = 0; +static bool m_IgnoreConfigChanges = false; static void M_SyncToWindow(void); -static void M_SyncFromWindow(void); +static void M_SyncFromWindow(bool update_viewport); +static bool M_MustUpdateRendererViewport(void); static void M_RefreshRendererViewport(void); static void M_HandleFocusGained(void); static void M_HandleFocusLost(void); @@ -135,9 +138,35 @@ static void M_SyncToWindow(void) width = 1280; height = 720; } - if (x <= 0 || y <= 0) { - x = (Shell_GetCurrentDisplayWidth() - width) / 2; - y = (Shell_GetCurrentDisplayHeight() - height) / 2; + + // Handle default position + if (x == -1 && y == -1) { + SDL_DisplayMode display_mode; + SDL_GetCurrentDisplayMode(0, &display_mode); + x = (display_mode.w - width) / 2; + y = (display_mode.h - height) / 2; + } else { + // Adjust window position if completely offscreen + bool on_screen = false; + const int32_t num_displays = SDL_GetNumVideoDisplays(); + for (int32_t i = 0; i < num_displays; i++) { + SDL_Rect bounds; + SDL_GetDisplayBounds(i, &bounds); + if (x + width > bounds.x && x < bounds.x + bounds.w + && y + height > bounds.y && y < bounds.y + bounds.h) { + on_screen = true; + break; + } + } + if (!on_screen) { + x = 0; + y = 0; + // Find the first display to reposition the window + SDL_Rect bounds; + SDL_GetDisplayBounds(0, &bounds); + x = bounds.x + (bounds.w - width) / 2; + y = bounds.y + (bounds.h - height) / 2; + } } SDL_SetWindowFullscreen(g_SDLWindow, 0); @@ -147,50 +176,54 @@ static void M_SyncToWindow(void) } } -static void M_SyncFromWindow(void) +static void M_SyncFromWindow(const bool update_viewport) { - if (SDL_GetTicks() - m_UpdateDebounce < 1000) { - // Setting the size programatically triggers resize events. - // Additionally, SDL_GetWindowSize() is not guaranteed to return the - // same values as passed to SDL_SetWindowSize(). In order to avoid - // infinite loops where the window dimensions are continuously updated, - // resize events are debounced. - return; - } + // Determine if this call should sync config, i.e., skip immediate + // programmatic events + const Uint32 now = SDL_GetTicks(); + const bool skip_config = (now - m_UpdateDebounce) < 500; + // Always pull current window state for logging and viewport reset const Uint32 window_flags = SDL_GetWindowFlags(g_SDLWindow); const bool is_maximized = window_flags & SDL_WINDOW_MAXIMIZED; - - int32_t width; - int32_t height; + int32_t x, y; + int32_t width, height; SDL_GetWindowSize(g_SDLWindow, &width, &height); - - int32_t x; - int32_t y; SDL_GetWindowPosition(g_SDLWindow, &x, &y); - LOG_INFO("%dx%d+%d,%d (maximized: %d)", width, height, x, y, is_maximized); - g_Config.window.is_maximized = is_maximized; - if (!is_maximized && !g_Config.window.is_fullscreen) { - g_Config.window.x = x; - g_Config.window.y = y; - g_Config.window.width = width; - g_Config.window.height = height; + // Update config only when not in debounce window + if (!skip_config) { + g_Config.window.is_maximized = is_maximized; + if (!is_maximized && !g_Config.window.is_fullscreen) { + g_Config.window.x = x; + g_Config.window.y = y; + g_Config.window.width = width; + g_Config.window.height = height; + } + if (g_Config.loaded) { + m_IgnoreConfigChanges = true; + Config_Write(); + m_IgnoreConfigChanges = false; + } } - // Save the updated config, but ensure it was loaded first - if (g_Config.loaded) { - Config_Write(); + if (update_viewport || M_MustUpdateRendererViewport()) { + // Refresh viewport to reflect the actual window size + M_RefreshRendererViewport(); } +} - M_RefreshRendererViewport(); +static bool M_MustUpdateRendererViewport(void) +{ + const SHELL_SIZE size = Shell_GetCurrentSize(); + return m_ViewportSize.w != size.w || m_ViewportSize.h != size.h; } static void M_RefreshRendererViewport(void) { Viewport_Reset(); - UI_Events_Fire(&(EVENT) { .name = "canvas_resize" }); + m_ViewportSize = Shell_GetCurrentSize(); } static void M_HandleFocusGained(void) @@ -208,7 +241,7 @@ static void M_HandleWindowShown(void) static void M_HandleWindowRestored(void) { - M_SyncFromWindow(); + M_SyncFromWindow(true); } static void M_HandleWindowMinimized(void) @@ -218,17 +251,17 @@ static void M_HandleWindowMinimized(void) static void M_HandleWindowMaximized(void) { - M_SyncFromWindow(); + M_SyncFromWindow(true); } static void M_HandleWindowMoved(const int32_t x, const int32_t y) { - M_SyncFromWindow(); + M_SyncFromWindow(false); } static void M_HandleWindowResized(int32_t width, int32_t height) { - M_SyncFromWindow(); + M_SyncFromWindow(true); } static void M_HandleKeyDown(const SDL_Event *const event) @@ -346,6 +379,10 @@ static void M_LoadConfig(void) static void M_HandleConfigChange(const EVENT *const event, void *const data) { + if (m_IgnoreConfigChanges) { + return; + } + const CONFIG *const old = &g_Config; const CONFIG *const new = &g_SavedConfig; @@ -359,9 +396,9 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) } if (CHANGED(window.is_fullscreen) || CHANGED(window.is_maximized) - || CHANGED(window.x) || CHANGED(window.y) || CHANGED(window.width) - || CHANGED(window.height) || CHANGED(rendering.scaler) - || CHANGED(rendering.sizer) || CHANGED(rendering.aspect_mode)) { + || CHANGED(window.width) || CHANGED(window.height) + || CHANGED(rendering.scaler) || CHANGED(rendering.sizer) + || CHANGED(rendering.aspect_mode)) { LOG_DEBUG("Change in settings detected"); M_SyncToWindow(); M_RefreshRendererViewport(); @@ -379,11 +416,17 @@ static void M_HandleConfigChange(const EVENT *const event, void *const data) Render_Reset(RENDER_RESET_PARAMS); } - if (CHANGED(visuals.fov) || CHANGED(visuals.use_pcx_fov)) { + if (CHANGED(visuals.fov) || CHANGED(visuals.use_psx_fov)) { if (Viewport_GetFOV(false) == -1) { Viewport_AlterFOV(-1); } } + + if (CHANGED(visuals.fog_start) || CHANGED(visuals.fog_end) + || CHANGED(visuals.water_color.g) || CHANGED(visuals.water_color.b) + || CHANGED(visuals.water_color.r)) { + Output_ApplyLevelSettings(); + } } // TODO: refactor the hell out of me @@ -439,7 +482,8 @@ void Shell_Main(void) Savegame_Init(); Savegame_InitCurrentInfo(); - S_FrontEndCheck(); + Savegame_ScanSavedGames(); + Savegame_HighlightNewestSlot(); if (m_Args.level_to_play != nullptr) { Memory_Free(g_GameFlow.level_tables[GFLT_MAIN].levels[0].path); @@ -476,14 +520,15 @@ void Shell_Main(void) } case GF_START_SAVED_GAME: { - if (!S_LoadGame(gf_cmd.param)) { + const int16_t slot_num = gf_cmd.param; + const int16_t level_num = Savegame_GetLevelNumber(slot_num); + if (level_num < 0) { + LOG_ERROR("Corrupt save file!"); gf_cmd = (GF_COMMAND) { .action = GF_EXIT_TO_TITLE }; } else { - const GF_LEVEL *const level = - GF_GetLevel(GFLT_MAIN, g_SaveGame.current_level); - if (level != nullptr) { - gf_cmd = GF_DoLevelSequence(level, GFSC_SAVED); - } + Savegame_BindSlot(slot_num); + const GF_LEVEL *const level = GF_GetLevel(GFLT_MAIN, level_num); + gf_cmd = GF_DoLevelSequence(level, GFSC_SAVED); } break; } @@ -565,12 +610,6 @@ void Shell_Start(void) M_RefreshRendererViewport(); } -bool Shell_IsFullscreen(void) -{ - const Uint32 flags = SDL_GetWindowFlags(g_SDLWindow); - return (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; -} - // TODO: try to call this function in a single place after introducing phases. void Shell_ProcessEvents(void) { diff --git a/src/tr2/game/sound.c b/src/tr2/game/sound.c index 16611bf49..1998b8d7a 100644 --- a/src/tr2/game/sound.c +++ b/src/tr2/game/sound.c @@ -13,8 +13,6 @@ #include -#define MAX_VOLUME 0x8000 - typedef enum { SOUND_MODE_NORMAL = 0, SOUND_MODE_WAIT = 1, @@ -44,7 +42,9 @@ typedef enum { #define SOUND_RADIUS_SQRD SQUARE(SOUND_RADIUS) // = 0x6400000 #define SOUND_MAX_SLOTS 32 -#define SOUND_MAX_VOLUME (SOUND_RADIUS - 1) +#define SOUND_RANGE_MULT_CONSTANT 4 +// sample volume ranges from 0..32767 +#define SOUND_MAX_VOLUME ((SOUND_RADIUS * SOUND_RANGE_MULT_CONSTANT) - 1) #define SOUND_MAX_VOLUME_CHANGE 0x2000 #define SOUND_MAX_PITCH_CHANGE 6000 @@ -69,11 +69,12 @@ static void M_ClearAllSlots(void); static void M_CloseSlot(SOUND_SLOT *const slot); static void M_UpdateSlot(SOUND_SLOT *const slot); -static int32_t M_ConvertVolumeToDecibel(const int32_t volume) +static int32_t M_ConvertVolumeToDecibel(int32_t volume) { - const double adjusted_volume = m_MasterVolume * volume; - const double scaler = 0x1.p-21; // 2.0e-21 - return (adjusted_volume * scaler - 1.0) * 5000.0; + int32_t idx = + volume * (m_MasterVolume / 64.0f) * DECIBEL_LUT_SIZE / SOUND_MAX_VOLUME; + CLAMP(idx, 0, DECIBEL_LUT_SIZE - 1); + return m_DecibelLUT[idx]; } static int32_t M_ConvertPanToDecibel(const uint16_t pan) @@ -281,8 +282,8 @@ bool Sound_Effect( } if (free_slot == -1) { - // No slot found - try to find the most silent track, and use this one - int32_t min_volume = MAX_VOLUME; + // No slot found - try to find the most quiet track, and use this one + int32_t min_volume = INT32_MAX; for (int32_t i = 0; i < SOUND_MAX_SLOTS; i++) { SOUND_SLOT *const slot = &m_SoundSlots[i]; if (slot->effect_num >= 0 && slot->volume < min_volume) { diff --git a/src/tr2/game/spawn.c b/src/tr2/game/spawn.c index ff4531dd9..e8c161787 100644 --- a/src/tr2/game/spawn.c +++ b/src/tr2/game/spawn.c @@ -134,7 +134,7 @@ int16_t Spawn_GunShot( effect->counter = 3; effect->frame_num = 0; effect->object_id = O_GUN_FLASH; - effect->shade = HIGH_LIGHT; + effect->shade = SHADE_NEUTRAL; return effect_num; } diff --git a/src/tr2/game/stats.c b/src/tr2/game/stats.c index b99d62d64..30a6ee15d 100644 --- a/src/tr2/game/stats.c +++ b/src/tr2/game/stats.c @@ -1,18 +1,21 @@ #include "game/stats.h" #include "game/clock.h" +#include "game/game.h" #include "game/game_flow.h" #include "game/objects/vars.h" +#include "game/savegame.h" #include "global/vars.h" #include +#include #define USE_REAL_CLOCK 0 static int32_t m_CachedItemCount = 0; static int32_t m_LevelSecrets = 0; -static bool M_SetSecretFlag(uint8_t *flags, GAME_OBJECT_ID obj_id); +static bool M_SetSecretFlag(uint16_t *flags, GAME_OBJECT_ID obj_id); #if USE_REAL_CLOCK static CLOCK_TIMER m_StartCounter = { .type = CLOCK_TYPE_REAL }; @@ -21,13 +24,16 @@ static int32_t m_StartTimer = 0; void Stats_StartTimer(void) { ClockTimer_Sync(&m_StartCounter); - m_StartTimer = g_SaveGame.current_stats.timer; + const RESUME_INFO *const resume = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + m_StartTimer = resume->stats.timer; } void Stats_UpdateTimer(void) { const double elapsed = ClockTimer_PeekElapsed(&m_StartCounter) * LOGIC_FPS; - g_SaveGame.current_stats.timer = m_StartTimer + elapsed; + RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + resume->stats.timer = m_StartTimer + elapsed; } #else void Stats_StartTimer(void) @@ -36,11 +42,15 @@ void Stats_StartTimer(void) void Stats_UpdateTimer(void) { - g_SaveGame.current_stats.timer++; + const GF_LEVEL *const level = Game_GetCurrentLevel(); + if (level != nullptr) { + RESUME_INFO *const resume = Savegame_GetCurrentInfo(level); + resume->stats.timer++; + } } #endif -static bool M_SetSecretFlag(uint8_t *const flags, const GAME_OBJECT_ID obj_id) +static bool M_SetSecretFlag(uint16_t *const flags, const GAME_OBJECT_ID obj_id) { for (int32_t i = 0; i < 2; i++) { const int32_t flag = 1 << ((obj_id - O_SECRET_1) + i * 3); @@ -53,7 +63,7 @@ static bool M_SetSecretFlag(uint8_t *const flags, const GAME_OBJECT_ID obj_id) return false; } -FINAL_STATS Stats_ComputeFinalStats(GF_LEVEL_TYPE level_type) +FINAL_STATS Stats_ComputeFinalStats(const GF_LEVEL_TYPE level_type) { FINAL_STATS result = {}; @@ -63,16 +73,18 @@ FINAL_STATS Stats_ComputeFinalStats(GF_LEVEL_TYPE level_type) if (level->type != level_type) { continue; } - result.timer += g_SaveGame.start[i].stats.timer; - result.ammo_used += g_SaveGame.start[i].stats.ammo_used; - result.ammo_hits += g_SaveGame.start[i].stats.ammo_hits; - result.kills += g_SaveGame.start[i].stats.kills; - result.distance += g_SaveGame.start[i].stats.distance; - result.medipacks += g_SaveGame.start[i].stats.medipacks; - for (int32_t j = 0; j < g_SaveGame.start[i].stats.max_secret_count; - j++) { - if (g_SaveGame.start[i].stats.secret_flags & (1 << j)) { + const RESUME_INFO *const info = Savegame_GetCurrentInfo(level); + const LEVEL_STATS *const stats = &info->stats; + result.timer += stats->timer; + result.ammo_used += stats->ammo_used; + result.ammo_hits += stats->ammo_hits; + result.kill_count += stats->kill_count; + result.distance_travelled += stats->distance_travelled; + result.medipacks_used += stats->medipacks_used; + + for (int32_t j = 0; j < stats->max_secret_count; j++) { + if (stats->secret_flags & (1 << j)) { result.found_secrets++; } result.total_secrets++; @@ -82,17 +94,6 @@ FINAL_STATS Stats_ComputeFinalStats(GF_LEVEL_TYPE level_type) return result; } -void Stats_Reset(void) -{ - g_SaveGame.current_stats.timer = 0; - g_SaveGame.current_stats.kills = 0; - g_SaveGame.current_stats.distance = 0; - g_SaveGame.current_stats.ammo_hits = 0; - g_SaveGame.current_stats.ammo_used = 0; - g_SaveGame.current_stats.medipacks = 0; - g_SaveGame.current_stats.secret_flags = 0; -} - void Stats_ObserveItemsLoad(void) { m_CachedItemCount = Item_GetLevelCount(); @@ -101,7 +102,7 @@ void Stats_ObserveItemsLoad(void) void Stats_CalculateStats(void) { m_LevelSecrets = 0; - uint8_t secret_flags = 0; + uint16_t secret_flags = 0; for (int32_t i = 0; i < m_CachedItemCount; i++) { const ITEM *const item = Item_Get(i); @@ -124,19 +125,22 @@ int32_t Stats_GetSecrets(void) void Stats_MarkSecretCollected(const GAME_OBJECT_ID obj_id) { - M_SetSecretFlag(&g_SaveGame.current_stats.secret_flags, obj_id); + RESUME_INFO *const resume = Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + M_SetSecretFlag(&resume->stats.secret_flags, obj_id); } bool Stats_CheckAllLevelSecretsCollected(void) { - int32_t flags = g_SaveGame.current_stats.secret_flags; + const RESUME_INFO *const resume = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + int32_t flags = resume->stats.secret_flags; int32_t count = 0; while (flags != 0) { count += flags & 1; flags >>= 1; } - return count >= g_SaveGame.current_stats.max_secret_count; + return count >= resume->stats.max_secret_count; } bool Stats_CheckAllSecretsCollected(GF_LEVEL_TYPE level_type) @@ -144,3 +148,40 @@ bool Stats_CheckAllSecretsCollected(GF_LEVEL_TYPE level_type) const FINAL_STATS stats = Stats_ComputeFinalStats(level_type); return stats.found_secrets >= stats.total_secrets; } + +void Stats_AddKill(void) +{ + RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + current_info->stats.kill_count++; +} + +void Stats_AddAmmoHits(void) +{ + RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + current_info->stats.ammo_hits++; +} + +void Stats_AddAmmoUsed(void) +{ + RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + current_info->stats.ammo_used++; +} + +void Stats_AddMedipacksUsed(const double medipack_value) +{ + RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + current_info->stats.medipacks_used += medipack_value; +} + +void Stats_AddDistanceTravelled(const XYZ_32 pos, const XYZ_32 last_pos) +{ + RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(Game_GetCurrentLevel()); + current_info->stats.distance_travelled += Math_Sqrt( + SQUARE(pos.z - last_pos.z) + SQUARE(pos.y - last_pos.y) + + SQUARE(pos.x - last_pos.x)); +} diff --git a/src/tr2/game/stats.h b/src/tr2/game/stats.h index 8dcdd9b1e..735801ac2 100644 --- a/src/tr2/game/stats.h +++ b/src/tr2/game/stats.h @@ -3,13 +3,18 @@ #include "global/types.h" #include +#include -void Stats_StartTimer(void); void Stats_UpdateTimer(void); -void Stats_Reset(void); void Stats_CalculateStats(void); int32_t Stats_GetSecrets(void); void Stats_MarkSecretCollected(GAME_OBJECT_ID obj_id); bool Stats_CheckAllLevelSecretsCollected(void); FINAL_STATS Stats_ComputeFinalStats(GF_LEVEL_TYPE level_type); bool Stats_CheckAllSecretsCollected(GF_LEVEL_TYPE level_type); + +void Stats_AddKill(void); +void Stats_AddAmmoHits(void); +void Stats_AddAmmoUsed(void); +void Stats_AddMedipacksUsed(double medipack_value); +void Stats_AddDistanceTravelled(XYZ_32 pos, XYZ_32 last_pos); diff --git a/src/tr2/game/text.c b/src/tr2/game/text.c index 7d7d57efd..7b7dff238 100644 --- a/src/tr2/game/text.c +++ b/src/tr2/game/text.c @@ -3,9 +3,9 @@ #include "decomp/decomp.h" #include "game/clock.h" #include "game/output.h" -#include "game/scaler.h" #include "global/vars.h" +#include #include static int32_t M_Scale(const int32_t value); @@ -15,38 +15,6 @@ static int32_t M_Scale(const int32_t value) return Scaler_Calc(value, SCALER_TARGET_TEXT); } -void Text_DrawBorder( - const int32_t x, const int32_t y, const int32_t z, const int32_t width, - const int32_t height) -{ - const int32_t mesh_idx = Object_Get(O_TEXT_BOX)->mesh_idx; - - const int32_t offset = 4; - const int32_t x0 = x + offset; - const int32_t y0 = y + offset; - const int32_t x1 = x0 + width - offset * 2; - const int32_t y1 = y0 + height - offset * 2; - const int32_t scale_h = TEXT_BASE_SCALE; - const int32_t scale_v = TEXT_BASE_SCALE; - - Output_DrawScreenSprite( - x0, y0, z, scale_h, scale_v, mesh_idx + 0, 0x1000, 0); - Output_DrawScreenSprite( - x1, y0, z, scale_h, scale_v, mesh_idx + 1, 0x1000, 0); - Output_DrawScreenSprite( - x1, y1, z, scale_h, scale_v, mesh_idx + 2, 0x1000, 0); - Output_DrawScreenSprite( - x0, y1, z, scale_h, scale_v, mesh_idx + 3, 0x1000, 0); - - int32_t w = (width - offset * 2) * TEXT_BASE_SCALE / 8; - int32_t h = (height - offset * 2) * TEXT_BASE_SCALE / 8; - - Output_DrawScreenSprite(x0, y0, z, w, scale_v, mesh_idx + 4, 0x1000, 0); - Output_DrawScreenSprite(x1, y0, z, scale_h, h, mesh_idx + 5, 0x1000, 0); - Output_DrawScreenSprite(x0, y1, z, w, scale_v, mesh_idx + 6, 0x1000, 0); - Output_DrawScreenSprite(x0, y0, z, scale_h, h, mesh_idx + 7, 0x1000, 0); -} - void Text_DrawText(TEXTSTRING *const text) { if (text->flags.drawn) { @@ -132,7 +100,8 @@ void Text_DrawText(TEXTSTRING *const text) const float sprite_scale = MIN(sprite_scale_h, sprite_scale_v); Output_DrawScreenSprite( x + M_Scale(10), y, z, M_Scale(glyph->width * sprite_scale), - M_Scale(glyph->width * sprite_scale), sprite_idx, 4096, 0); + M_Scale(glyph->width * sprite_scale), sprite_idx, SHADE_NEUTRAL, + 0); x += glyph->width * scale_h / TEXT_BASE_SCALE; goto loop_end; } @@ -148,7 +117,7 @@ void Text_DrawText(TEXTSTRING *const text) Output_DrawScreenSprite( cx, cy, 0, scale_h, scale_v, - obj->mesh_idx + glyph->combine_with.mesh_idx, 4096, 0); + obj->mesh_idx + glyph->combine_with.mesh_idx, SHADE_NEUTRAL, 0); } if (x >= 0 && x < g_PhdWinWidth && y >= 0 && y < g_PhdWinHeight) { @@ -157,7 +126,7 @@ void Text_DrawText(TEXTSTRING *const text) } Output_DrawScreenSprite( x, y, z, scale_h, scale_v, obj->mesh_idx + glyph->mesh_idx, - 4096, 0); + SHADE_NEUTRAL, 0); } if (glyph->role != GLYPH_COMBINING) { @@ -186,13 +155,14 @@ void Text_DrawText(TEXTSTRING *const text) } if (text->flags.background) { - Output_DrawScreenFBox( - box_x, box_y, z + text->background.offset.z, box_w, box_h, 0, - nullptr, 0); + Output_DrawTextBackground( + UI_STYLE_PC, box_x, box_y, box_w, box_h, + z + text->background.offset.z, TS_REQUESTED); } if (text->flags.outline) { - Text_DrawBorder(box_x, box_y, z, box_w, box_h); + Output_DrawTextOutline( + UI_STYLE_PC, box_x, box_y, box_w, box_h, z, TS_REQUESTED); } } diff --git a/src/tr2/game/text.h b/src/tr2/game/text.h index 05b8687d8..47addce84 100644 --- a/src/tr2/game/text.h +++ b/src/tr2/game/text.h @@ -6,6 +6,4 @@ #define TEXT_HEIGHT 15 -void Text_DrawBorder( - int32_t x, int32_t y, int32_t z, int32_t width, int32_t height); void Text_DrawText(TEXTSTRING *text); diff --git a/src/tr2/game/ui/common.c b/src/tr2/game/ui/common.c index 55b7f2ddf..f4b10ec67 100644 --- a/src/tr2/game/ui/common.c +++ b/src/tr2/game/ui/common.c @@ -1,35 +1,25 @@ -#include "game/scaler.h" #include "global/vars.h" #include +#include #include -#include - int32_t UI_GetCanvasWidth(void) { - return Scaler_CalcInverse(g_PhdWinWidth, SCALER_TARGET_TEXT); + return Scaler_CalcInverse(g_PhdWinWidth, SCALER_TARGET_GENERIC); } int32_t UI_GetCanvasHeight(void) { - return Scaler_CalcInverse(g_PhdWinHeight, SCALER_TARGET_TEXT); + return Scaler_CalcInverse(g_PhdWinHeight, SCALER_TARGET_GENERIC); } -UI_INPUT UI_TranslateInput(uint32_t system_keycode) +float UI_ScaleX(const float x) { - // clang-format off - switch (system_keycode) { - case SDLK_UP: return UI_KEY_UP; - case SDLK_DOWN: return UI_KEY_DOWN; - case SDLK_LEFT: return UI_KEY_LEFT; - case SDLK_RIGHT: return UI_KEY_RIGHT; - case SDLK_HOME: return UI_KEY_HOME; - case SDLK_END: return UI_KEY_END; - case SDLK_BACKSPACE: return UI_KEY_BACK; - case SDLK_RETURN: return UI_KEY_RETURN; - case SDLK_ESCAPE: return UI_KEY_ESCAPE; - } - // clang-format on - return -1; + return Scaler_Calc(x, SCALER_TARGET_GENERIC); +} + +float UI_ScaleY(const float y) +{ + return Scaler_Calc(y, SCALER_TARGET_GENERIC); } diff --git a/src/tr2/game/ui/controllers/controls.c b/src/tr2/game/ui/controllers/controls.c deleted file mode 100644 index 6012394c4..000000000 --- a/src/tr2/game/ui/controllers/controls.c +++ /dev/null @@ -1,285 +0,0 @@ -#include "game/ui/controllers/controls.h" - -#include "game/input.h" -#include "game/shell.h" -#include "global/vars.h" - -#include -#include -#include -#include - -static const INPUT_ROLE m_LeftRoles[] = { - // clang-format off - INPUT_ROLE_UP, - INPUT_ROLE_DOWN, - INPUT_ROLE_LEFT, - INPUT_ROLE_RIGHT, - INPUT_ROLE_STEP_L, - INPUT_ROLE_STEP_R, - INPUT_ROLE_SLOW, - INPUT_ROLE_ENTER_CONSOLE, - INPUT_ROLE_PAUSE, - INPUT_ROLE_TOGGLE_PHOTO_MODE, - INPUT_ROLE_TOGGLE_UI, - // INPUT_ROLE_CAMERA_RESET, // same as look, no need to configure - INPUT_ROLE_CAMERA_UP, - INPUT_ROLE_CAMERA_DOWN, - INPUT_ROLE_CAMERA_LEFT, - INPUT_ROLE_CAMERA_RIGHT, - INPUT_ROLE_CAMERA_FORWARD, - INPUT_ROLE_CAMERA_BACK, - (INPUT_ROLE)-1, - // clang-format on -}; - -static const INPUT_ROLE m_RightRoles_CheatsOff[] = { - // clang-format off - INPUT_ROLE_JUMP, - INPUT_ROLE_ACTION, - INPUT_ROLE_DRAW, - INPUT_ROLE_USE_FLARE, - INPUT_ROLE_LOOK, - INPUT_ROLE_ROLL, - INPUT_ROLE_OPTION, - (INPUT_ROLE)-1, - // clang-format on -}; - -static const INPUT_ROLE m_RightRoles_CheatsOn[] = { - // clang-format off - INPUT_ROLE_JUMP, - INPUT_ROLE_ACTION, - INPUT_ROLE_DRAW, - INPUT_ROLE_USE_FLARE, - INPUT_ROLE_LOOK, - INPUT_ROLE_ROLL, - INPUT_ROLE_OPTION, - INPUT_ROLE_FLY_CHEAT, - INPUT_ROLE_ITEM_CHEAT, - INPUT_ROLE_LEVEL_SKIP_CHEAT, - INPUT_ROLE_TURBO_CHEAT, - (INPUT_ROLE)-1, - // clang-format on -}; - -static INPUT_ROLE M_GetInputRole(int32_t col, int32_t row); -static void M_CycleLayout(UI_CONTROLS_CONTROLLER *controller, int32_t dir); -static bool M_NavigateLayout(UI_CONTROLS_CONTROLLER *controller); -static bool M_NavigateInputs(UI_CONTROLS_CONTROLLER *controller); -static bool M_NavigateInputsDebounce(UI_CONTROLS_CONTROLLER *controller); -static bool M_Listen(UI_CONTROLS_CONTROLLER *controller); -static bool M_ListenDebounce(UI_CONTROLS_CONTROLLER *controller); - -static INPUT_ROLE M_GetInputRole(const int32_t col, const int32_t row) -{ - if (col == 0) { - return m_LeftRoles[row]; - } else if (g_Config.gameplay.enable_cheats) { - return m_RightRoles_CheatsOn[row]; - } else { - return m_RightRoles_CheatsOff[row]; - } -} - -static void M_CycleLayout( - UI_CONTROLS_CONTROLLER *const controller, const int32_t dir) -{ - controller->active_layout += dir; - controller->active_layout += INPUT_LAYOUT_NUMBER_OF; - controller->active_layout %= INPUT_LAYOUT_NUMBER_OF; - - const EVENT event = { - .name = "layout_change", - .sender = nullptr, - .data = nullptr, - }; - EventManager_Fire(controller->events, &event); -} - -static bool M_NavigateBackend(UI_CONTROLS_CONTROLLER *const controller) -{ - if (g_InputDB.menu_down && controller->backend == INPUT_BACKEND_KEYBOARD) { - controller->backend = INPUT_BACKEND_CONTROLLER; - return true; - } - - if (g_InputDB.menu_up && controller->backend == INPUT_BACKEND_CONTROLLER) { - controller->backend = INPUT_BACKEND_KEYBOARD; - return true; - } - - if (g_InputDB.menu_back) { - controller->state = UI_CONTROLS_STATE_EXIT; - return true; - } - - if (g_InputDB.menu_confirm) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_LAYOUT; - return true; - } - - return false; -} - -static bool M_NavigateLayout(UI_CONTROLS_CONTROLLER *const controller) -{ - if (g_InputDB.menu_confirm) { - controller->state = UI_CONTROLS_STATE_EXIT; - } else if (g_InputDB.menu_back) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_BACKEND; - } else if (g_InputDB.menu_left) { - M_CycleLayout(controller, -1); - } else if (g_InputDB.menu_right) { - M_CycleLayout(controller, 1); - } else if (g_InputDB.menu_down && controller->active_layout != 0) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_INPUTS; - controller->active_col = 0; - controller->active_row = 0; - } else if (g_InputDB.menu_up && controller->active_layout != 0) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_INPUTS; - controller->active_col = 1; - controller->active_row = UI_ControlsController_GetInputRoleCount(1) - 1; - } else { - return false; - } - controller->active_role = - M_GetInputRole(controller->active_col, controller->active_row); - return true; -} - -static bool M_NavigateInputs(UI_CONTROLS_CONTROLLER *const controller) -{ - if (g_InputDB.menu_back) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_BACKEND; - } else if (g_InputDB.menu_left || g_InputDB.menu_right) { - controller->active_col ^= 1; - CLAMP( - controller->active_row, 0, - UI_ControlsController_GetInputRoleCount(controller->active_col) - - 1); - } else if (g_InputDB.menu_up) { - controller->active_row--; - if (controller->active_row < 0) { - if (controller->active_col == 0) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_LAYOUT; - } else { - controller->active_col = 0; - controller->active_row = - UI_ControlsController_GetInputRoleCount(0) - 1; - } - } - } else if (g_InputDB.menu_down) { - controller->active_row++; - if (controller->active_row >= UI_ControlsController_GetInputRoleCount( - controller->active_col)) { - if (controller->active_col == 0) { - controller->active_col = 1; - controller->active_row = 0; - } else { - controller->state = UI_CONTROLS_STATE_NAVIGATE_LAYOUT; - } - } - } else if (g_InputDB.menu_confirm) { - controller->state = UI_CONTROLS_STATE_NAVIGATE_INPUTS_DEBOUNCE; - } else { - return false; - } - controller->active_role = - M_GetInputRole(controller->active_col, controller->active_row); - return true; -} - -static bool M_NavigateInputsDebounce(UI_CONTROLS_CONTROLLER *const controller) -{ - Shell_ProcessEvents(); - Input_Update(); - if (g_Input.any) { - return false; - } - Input_EnterListenMode(); - controller->state = UI_CONTROLS_STATE_LISTEN; - return true; -} - -static bool M_Listen(UI_CONTROLS_CONTROLLER *const controller) -{ - if (!Input_ReadAndAssignRole( - controller->backend, controller->active_layout, - controller->active_role)) { - return false; - } - - Input_ExitListenMode(); - - const EVENT event = { - .name = "key_change", - .sender = nullptr, - .data = nullptr, - }; - EventManager_Fire(controller->events, &event); - - controller->state = UI_CONTROLS_STATE_LISTEN_DEBOUNCE; - return true; -} - -static bool M_ListenDebounce(UI_CONTROLS_CONTROLLER *const controller) -{ - if (g_Input.any) { - return false; - } - - controller->state = UI_CONTROLS_STATE_NAVIGATE_INPUTS; - return true; -} - -void UI_ControlsController_Init(UI_CONTROLS_CONTROLLER *const controller) -{ - ASSERT(controller->events == nullptr); - controller->backend = INPUT_BACKEND_KEYBOARD; - controller->state = UI_CONTROLS_STATE_NAVIGATE_BACKEND; - - controller->events = EventManager_Create(); -} - -void UI_ControlsController_Shutdown(UI_CONTROLS_CONTROLLER *const controller) -{ - EventManager_Free(controller->events); - controller->events = nullptr; -} - -bool UI_ControlsController_Control(UI_CONTROLS_CONTROLLER *const controller) -{ - switch (controller->state) { - case UI_CONTROLS_STATE_NAVIGATE_BACKEND: - return M_NavigateBackend(controller); - case UI_CONTROLS_STATE_NAVIGATE_LAYOUT: - return M_NavigateLayout(controller); - case UI_CONTROLS_STATE_NAVIGATE_INPUTS: - return M_NavigateInputs(controller); - case UI_CONTROLS_STATE_NAVIGATE_INPUTS_DEBOUNCE: - return M_NavigateInputsDebounce(controller); - case UI_CONTROLS_STATE_LISTEN: - return M_Listen(controller); - case UI_CONTROLS_STATE_LISTEN_DEBOUNCE: - return M_ListenDebounce(controller); - default: - return false; - } - return false; -} - -INPUT_ROLE UI_ControlsController_GetInputRole( - const int32_t col, const int32_t row) -{ - return M_GetInputRole(col, row); -} - -int32_t UI_ControlsController_GetInputRoleCount(const int32_t col) -{ - int32_t row = 0; - while (M_GetInputRole(col, row) != (INPUT_ROLE)-1) { - row++; - } - return row; -} diff --git a/src/tr2/game/ui/controllers/controls.h b/src/tr2/game/ui/controllers/controls.h deleted file mode 100644 index 60d4985d2..000000000 --- a/src/tr2/game/ui/controllers/controls.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "game/input.h" - -#include - -typedef enum { - UI_CONTROLS_STATE_NAVIGATE_BACKEND, - UI_CONTROLS_STATE_NAVIGATE_LAYOUT, - UI_CONTROLS_STATE_NAVIGATE_INPUTS, - UI_CONTROLS_STATE_NAVIGATE_INPUTS_DEBOUNCE, - UI_CONTROLS_STATE_LISTEN, - UI_CONTROLS_STATE_LISTEN_DEBOUNCE, - UI_CONTROLS_STATE_EXIT, -} UI_CONTROLS_STATE; - -typedef struct { - INPUT_BACKEND backend; - UI_CONTROLS_STATE state; - int32_t active_layout; - INPUT_ROLE active_role; - int32_t active_col; - int32_t active_row; - - EVENT_MANAGER *events; -} UI_CONTROLS_CONTROLLER; - -void UI_ControlsController_Init(UI_CONTROLS_CONTROLLER *controller); -void UI_ControlsController_Shutdown(UI_CONTROLS_CONTROLLER *controller); -bool UI_ControlsController_Control(UI_CONTROLS_CONTROLLER *controller); - -INPUT_ROLE UI_ControlsController_GetInputRole(int32_t col, int32_t row); -int32_t UI_ControlsController_GetInputRoleCount(int32_t col); diff --git a/src/tr2/game/ui/dialogs/graphic_settings.c b/src/tr2/game/ui/dialogs/graphic_settings.c new file mode 100644 index 000000000..75ee7a212 --- /dev/null +++ b/src/tr2/game/ui/dialogs/graphic_settings.c @@ -0,0 +1,503 @@ +#include "game/ui/dialogs/graphic_settings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +typedef struct { + CONFIG_OPTION_TYPE option_type; + GAME_STRING_ID label_id; + void *target; + int32_t min_value; + int32_t max_value; + int32_t delta_slow; + int32_t delta_fast; + const void *misc; +} M_OPTION; + +typedef struct { + int32_t value; + GAME_STRING_ID name; +} M_ENUM_ENTRY; + +typedef struct { + const M_ENUM_ENTRY *entry; + int32_t position; + int32_t count; +} M_ENUM_LOOKUP; + +static const M_ENUM_ENTRY m_TextureFilterOptions[] = { + // clang-format off + { GFX_TF_NN, GS_ID(MISC_OFF), }, + { GFX_TF_BILINEAR, GS_ID(DETAIL_BILINEAR), }, + { -1, nullptr, }, + // clang-format on +}; + +static const M_ENUM_ENTRY m_LightingContrastOptions[] = { + // clang-format off + { LIGHTING_CONTRAST_LOW, GS_ID(DETAIL_LIGHTING_CONTRAST_LOW), }, + { LIGHTING_CONTRAST_MEDIUM, GS_ID(DETAIL_LIGHTING_CONTRAST_MEDIUM), }, + { LIGHTING_CONTRAST_HIGH, GS_ID(DETAIL_LIGHTING_CONTRAST_HIGH), }, + { -1, nullptr, }, + // clang-format on +}; + +static const M_ENUM_ENTRY m_RenderModeOptions[] = { + // clang-format off + { RM_SOFTWARE, GS_ID(DETAIL_RENDER_MODE_SOFTWARE), }, + { RM_HARDWARE, GS_ID(DETAIL_RENDER_MODE_HARDWARE), }, + { -1, nullptr, }, + // clang-format on +}; + +static const M_ENUM_ENTRY m_AspectModeOptions[] = { + // clang-format off + { AM_4_3, GS_ID(DETAIL_ASPECT_MODE_4_3), }, + { AM_16_9, GS_ID(DETAIL_ASPECT_MODE_16_9), }, + { AM_ANY, GS_ID(DETAIL_ASPECT_MODE_ANY), }, + { -1, nullptr, }, + // clang-format on +}; + +static const M_OPTION m_Options[] = { + { + .option_type = COT_INT32, + .label_id = GS_ID(DETAIL_FPS), + .target = &g_Config.rendering.fps, + .min_value = 30, + .max_value = 60, + .delta_slow = 30, + .delta_fast = 30, + }, + + { + .option_type = COT_INT32, + .label_id = GS_ID(DETAIL_FOG_START), + .target = &g_Config.visuals.fog_start, + .min_value = 1, + .max_value = 100, + }, + + { + .option_type = COT_INT32, + .label_id = GS_ID(DETAIL_FOG_END), + .target = &g_Config.visuals.fog_end, + .min_value = 1, + .max_value = 100, + }, + + { + .option_type = COT_RGB888, + .label_id = GS_ID(DETAIL_WATER_COLOR_R), + .target = &g_Config.visuals.water_color, + .min_value = 0, + .max_value = 255, + .delta_slow = 1, + .delta_fast = 10, + .misc = (void *)(intptr_t)0, + }, + + { + .option_type = COT_RGB888, + .label_id = GS_ID(DETAIL_WATER_COLOR_G), + .target = &g_Config.visuals.water_color, + .min_value = 0, + .max_value = 255, + .delta_slow = 1, + .delta_fast = 10, + .misc = (void *)(intptr_t)1, + }, + + { + .option_type = COT_RGB888, + .label_id = GS_ID(DETAIL_WATER_COLOR_B), + .target = &g_Config.visuals.water_color, + .min_value = 0, + .max_value = 255, + .delta_slow = 1, + .delta_fast = 10, + .misc = (void *)(intptr_t)2, + }, + + { + .option_type = COT_INT32, + .label_id = GS_ID(DETAIL_FOV), + .target = &g_Config.visuals.fov, + .min_value = 30, + .max_value = 150, + .delta_slow = 1, + .delta_fast = 10, + }, + + { + .option_type = COT_BOOL, + .label_id = GS_ID(DETAIL_USE_PSX_FOV), + .target = &g_Config.visuals.use_psx_fov, + }, + + { + .option_type = COT_ENUM, + .label_id = GS_ID(DETAIL_TEXTURE_FILTER), + .target = &g_Config.rendering.texture_filter, + .delta_slow = 1, + .delta_fast = 1, + .misc = m_TextureFilterOptions, + }, + + { + .option_type = COT_BOOL, + .label_id = GS_ID(DETAIL_TRAPEZOID_FILTER), + .target = &g_Config.rendering.enable_trapezoid_filter, + }, + + { + .option_type = COT_BOOL, + .label_id = GS_ID(DETAIL_DEPTH_BUFFER), + .target = &g_Config.rendering.enable_zbuffer, + }, + + { + .option_type = COT_ENUM, + .label_id = GS_ID(DETAIL_LIGHTING_CONTRAST), + .target = &g_Config.rendering.lighting_contrast, + .delta_slow = 1, + .delta_fast = 1, + .misc = m_LightingContrastOptions, + }, + + { + .option_type = COT_ENUM, + .label_id = GS_ID(DETAIL_RENDER_MODE), + .target = &g_Config.rendering.render_mode, + .delta_slow = 1, + .delta_fast = 1, + .misc = m_RenderModeOptions, + }, + + { + .option_type = COT_ENUM, + .label_id = GS_ID(DETAIL_ASPECT_MODE), + .target = &g_Config.rendering.aspect_mode, + .delta_slow = 1, + .delta_fast = 1, + .misc = m_AspectModeOptions, + }, + + { + .option_type = COT_DOUBLE, + .label_id = GS_ID(DETAIL_UI_TEXT_SCALE), + .target = &g_Config.ui.text_scale, + .min_value = 50, + .max_value = 200, + .delta_slow = 10, + .delta_fast = 10, + }, + + { + .option_type = COT_DOUBLE, + .label_id = GS_ID(DETAIL_UI_BAR_SCALE), + .target = &g_Config.ui.bar_scale, + .min_value = 50, + .max_value = 200, + .delta_slow = 10, + .delta_fast = 10, + }, + + { + .option_type = COT_INT32, + .label_id = GS_ID(DETAIL_SCALER), + .target = &g_Config.rendering.scaler, + .min_value = 1, + .max_value = 4, + .delta_slow = 1, + .delta_fast = 1, + }, + + { + .option_type = COT_FLOAT, + .label_id = GS_ID(DETAIL_SIZER), + .target = &g_Config.rendering.sizer, + .min_value = 40, + .max_value = 200, + .delta_slow = 10, + .delta_fast = 10, + }, + + { + .target = nullptr, + }, +}; + +static uint8_t *M_GetColorComponent(const M_OPTION *option); +static M_ENUM_LOOKUP M_GetEnumEntry(const M_OPTION *option); +static char *M_FormatRowValue(int32_t row_idx); +static bool M_CanChangeValue(int32_t row_idx, int32_t dir); +static bool M_RequestChangeValue(int32_t row_idx, int32_t dir); + +static uint8_t *M_GetColorComponent(const M_OPTION *const option) +{ + RGB_888 *const color = option->target; + switch ((int32_t)(intptr_t)option->misc) { + case 0: + return &color->r; + case 1: + return &color->g; + case 2: + return &color->b; + } + ASSERT_FAIL(); + return nullptr; +} + +static M_ENUM_LOOKUP M_GetEnumEntry(const M_OPTION *const option) +{ + M_ENUM_LOOKUP result = { + .entry = nullptr, + .position = -1, + .count = 0, + }; + int32_t current_pos = 0; + const M_ENUM_ENTRY *entry = &((M_ENUM_ENTRY *)option->misc)[0]; + while (entry->value != -1) { + if (entry->value == *(int32_t *)option->target) { + result.entry = entry; + result.position = current_pos; + } + entry++; + current_pos++; + result.count++; + } + return result; +} + +static char *M_FormatRowValue(const int32_t row_idx) +{ + const M_OPTION *const option = &m_Options[row_idx]; + switch (option->option_type) { + case COT_BOOL: + return String_Format( + "%s", *(bool *)option->target ? GS(MISC_ON) : GS(MISC_OFF)); + case COT_INT32: + return String_Format( + GS(DETAIL_INTEGER_FMT), *(int32_t *)option->target); + case COT_DOUBLE: + return String_Format(GS(DETAIL_FLOAT_FMT), *(double *)option->target); + case COT_FLOAT: + return String_Format(GS(DETAIL_FLOAT_FMT), *(float *)option->target); + case COT_RGB888: { + const uint8_t *const component = M_GetColorComponent(option); + return String_Format("%d", *component); + } + case COT_ENUM: { + const M_ENUM_LOOKUP enum_lookup = M_GetEnumEntry(option); + ASSERT(enum_lookup.entry != nullptr); + return (char *)GameString_Get(enum_lookup.entry->name); + } + default: + break; + } + return nullptr; +} + +static bool M_CanChangeValue(const int32_t row_idx, const int32_t dir) +{ + const M_OPTION *const option = &m_Options[row_idx]; + switch (option->option_type) { + case COT_BOOL: + return true; + case COT_INT32: + if (dir < 0) { + return *(int32_t *)option->target > option->min_value; + } else if (dir > 0) { + return *(int32_t *)option->target < option->max_value; + } + break; + case COT_DOUBLE: + if (dir < 0) { + return *(double *)option->target > (double)option->min_value / 100; + } else if (dir > 0) { + return *(double *)option->target < (double)option->max_value / 100; + } + break; + case COT_FLOAT: + if (dir < 0) { + return *(float *)option->target > (float)option->min_value / 100; + } else if (dir > 0) { + return *(float *)option->target < (float)option->max_value / 100; + } + break; + case COT_RGB888: { + const uint8_t *const component = M_GetColorComponent(option); + if (dir < 0) { + return *component > option->min_value; + } else if (dir > 0) { + return *component < option->max_value; + } + break; + } + case COT_ENUM: { + const M_ENUM_LOOKUP enum_lookup = M_GetEnumEntry(option); + ASSERT(enum_lookup.entry != nullptr); + if (dir < 0) { + return enum_lookup.position > 0; + } else if (dir > 0) { + return enum_lookup.position < enum_lookup.count - 1; + } + break; + } + default: + break; + } + return false; +} + +static bool M_RequestChangeValue(const int32_t row_idx, const int32_t dir) +{ + if (!M_CanChangeValue(row_idx, dir)) { + return false; + } + + const M_OPTION *const option = &m_Options[row_idx]; + int32_t delta = g_Input.slow ? option->delta_slow : option->delta_fast; + if (delta == 0) { + delta = 1; + } + delta *= dir; + + switch (option->option_type) { + case COT_BOOL: + *(bool *)option->target = !*(bool *)option->target; + break; + case COT_INT32: + *(int32_t *)option->target += delta; + break; + case COT_DOUBLE: + *(double *)option->target = + (round(*(double *)option->target * 10) + (delta / 10.0f)) / 10.0f; + break; + case COT_FLOAT: + *(float *)option->target = + (round(*(float *)option->target * 10) + (delta / 10.0f)) / 10.0f; + break; + case COT_RGB888: { + uint8_t *const component = M_GetColorComponent(option); + int32_t component_i = *component; + component_i += delta; + CLAMP(component_i, 0, 255); + *component = component_i; + break; + } + case COT_ENUM: { + const M_ENUM_LOOKUP enum_lookup = M_GetEnumEntry(option); + const M_ENUM_ENTRY *const next_entry = + &((M_ENUM_ENTRY *)option->misc)[enum_lookup.position + delta]; + *(int32_t *)option->target = next_entry->value; + break; + } + default: + return false; + } + Config_Write(); + return true; +} + +void UI_GraphicSettings_Init(UI_GRAPHIC_SETTINGS_STATE *const s) +{ + int32_t row_count = 0; + for (int32_t i = 0; m_Options[i].target != nullptr; i++) { + row_count++; + } + UI_Requester_Init(&s->req, row_count, row_count, true); + s->req.row_pad = 2.0f; + s->req.row_spacing = 0.0f; +} + +void UI_GraphicSettings_Free(UI_GRAPHIC_SETTINGS_STATE *const s) +{ + UI_Requester_Free(&s->req); +} + +bool UI_GraphicSettings_Control(UI_GRAPHIC_SETTINGS_STATE *const s) +{ + const int32_t scale = Scaler_GetScale(SCALER_TARGET_TEXT) * 100; + if (scale >= 190) { + UI_Requester_SetVisibleRows(&s->req, 6); + } else if (scale >= 160) { + UI_Requester_SetVisibleRows(&s->req, 8); + } else if (scale >= 120) { + UI_Requester_SetVisibleRows(&s->req, 12); + } else { + UI_Requester_SetVisibleRows(&s->req, 18); + } + + const int32_t choice = UI_Requester_Control(&s->req); + if (choice == UI_REQUESTER_CANCEL) { + return true; + } + const int32_t sel_row = UI_Requester_GetCurrentRow(&s->req); + if (g_InputDB.menu_left && sel_row >= 0) { + M_RequestChangeValue(sel_row, -1); + } else if (g_InputDB.menu_right && sel_row >= 0) { + M_RequestChangeValue(sel_row, +1); + } + return false; +} + +void UI_GraphicSettings(UI_GRAPHIC_SETTINGS_STATE *const s) +{ + const int32_t sel_row = UI_Requester_GetCurrentRow(&s->req); + UI_BeginModal(0.5f, 0.6f); + UI_BeginRequester(&s->req, GS(DETAIL_TITLE)); + + for (int32_t i = 0; i < s->req.max_rows; i++) { + if (!UI_Requester_IsRowVisible(&s->req, i)) { + UI_BeginResize(-1.0f, 0.0f); + } else { + UI_BeginResize(-1.0f, -1.0f); + } + + UI_BeginRequesterRow(&s->req, i); + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + }); + UI_Label(GameString_Get(m_Options[i].label_id)); + UI_Spacer(20.0f, 0.0f); + + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + .spacing = { .h = 5.0f }, + }); + UI_BeginHide(i != sel_row || !M_CanChangeValue(i, -1)); + UI_Label("\\{button left}"); + UI_EndHide(); + UI_Label(M_FormatRowValue(i)); + UI_BeginHide(i != sel_row || !M_CanChangeValue(i, +1)); + UI_Label("\\{button right}"); + UI_EndHide(); + UI_EndStack(); + + UI_EndStack(); + UI_EndRequesterRow(&s->req, i); + UI_EndResize(); + } + UI_EndRequester(&s->req); + UI_EndModal(); +} diff --git a/src/tr2/game/ui/dialogs/graphic_settings.h b/src/tr2/game/ui/dialogs/graphic_settings.h new file mode 100644 index 000000000..00d02a526 --- /dev/null +++ b/src/tr2/game/ui/dialogs/graphic_settings.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +typedef struct { + UI_REQUESTER_STATE req; +} UI_GRAPHIC_SETTINGS_STATE; + +void UI_GraphicSettings_Init(UI_GRAPHIC_SETTINGS_STATE *s); +void UI_GraphicSettings_Free(UI_GRAPHIC_SETTINGS_STATE *s); +bool UI_GraphicSettings_Control(UI_GRAPHIC_SETTINGS_STATE *s); + +void UI_GraphicSettings(UI_GRAPHIC_SETTINGS_STATE *s); diff --git a/src/tr2/game/ui/dialogs/stats.c b/src/tr2/game/ui/dialogs/stats.c new file mode 100644 index 000000000..9a3c275e7 --- /dev/null +++ b/src/tr2/game/ui/dialogs/stats.c @@ -0,0 +1,318 @@ +#include "game/ui/dialogs/stats.h" + +#include "game/game_flow.h" +#include "game/game_string.h" +#include "game/savegame.h" +#include "game/stats.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +typedef enum { + M_ROW_GENERIC, + M_ROW_TIMER, + M_ROW_LEVEL_SECRETS, + M_ROW_ALL_SECRETS, + M_ROW_KILLS, + M_ROW_AMMO_USED, + M_ROW_AMMO_HITS, + M_ROW_MEDIPACKS, + M_ROW_DISTANCE_TRAVELLED, +} M_ROW_ROLE; + +static void M_FormatTime(char *out, int32_t total_frames); +static void M_FormatSecrets(char *out, const LEVEL_STATS *level_stats); +static void M_FormatDistance(char *const out, int32_t distance); +static void M_Row( + const UI_STATS_DIALOG_STATE *s, const char *key, const char *value); +static void M_RowFromRole( + const UI_STATS_DIALOG_STATE *s, M_ROW_ROLE role, const STATS_COMMON *stats); +static void M_LevelStatsRows(const UI_STATS_DIALOG_STATE *s); +static void M_FinalStatsRows(const UI_STATS_DIALOG_STATE *s); +static void M_AssaultCourseStatsRows(UI_STATS_DIALOG_STATE *s); +static const char *M_GetDialogTitle(const UI_STATS_DIALOG_STATE *s); +static void M_BeginDialog(const UI_STATS_DIALOG_STATE *s); +static void M_EndDialog(const UI_STATS_DIALOG_STATE *s); + +static void M_FormatTime(char *const out, const int32_t total_frames) +{ + const int32_t total_seconds = total_frames / LOGIC_FPS; + const int32_t hours = total_seconds / 3600; + const int32_t minutes = (total_seconds / 60) % 60; + const int32_t seconds = total_seconds % 60; + sprintf(out, "%02d:%02d:%02d", hours, minutes, seconds); +} + +static void M_FormatSecrets( + char *const out, const LEVEL_STATS *const level_stats) +{ + char *ptr = out; + int32_t num_secrets = 0; + for (int32_t i = 1; i >= 0; i--) { + for (int32_t j = 0; j < 3; j++) { + const int32_t flag = 1 << (j + i * 3); + if ((level_stats->secret_flags & flag) != 0) { + sprintf(ptr, "\\{secret %d}", j + 1); + num_secrets++; + } else { + strcpy(ptr, " "); + } + ptr += strlen(ptr); + } + } + + *ptr++ = '\0'; + if (num_secrets == 0) { + strcpy(out, GS(MISC_NONE)); + } +} + +static void M_FormatDistance(char *const out, int32_t distance) +{ + distance /= 445; + if (distance < 1000) { + sprintf(out, "%dm", distance); + } else { + sprintf(out, "%d.%02dkm", distance / 1000, (distance % 1000) / 10); + } +} + +static void M_Row( + const UI_STATS_DIALOG_STATE *const s, const char *const key, + const char *const value) +{ + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_HORIZONTAL, + .spacing = { .h = TR_VERSION == 1 ? 30.0f : 90.0f }, + .align = { .h = UI_STACK_H_ALIGN_DISTRIBUTE }, + }); + UI_Label(key); + UI_Label(value); + UI_EndStack(); +} + +static void M_RowFromRole( + const UI_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role, + const STATS_COMMON *const stats) +{ + char buf[64]; + + switch (role) { + case M_ROW_TIMER: { + M_FormatTime(buf, stats->timer); + M_Row(s, GS(STATS_TIME_TAKEN), buf); + break; + } + + case M_ROW_LEVEL_SECRETS: { + M_FormatSecrets(buf, (LEVEL_STATS *)stats); + M_Row(s, GS(STATS_SECRETS), buf); + break; + } + + case M_ROW_ALL_SECRETS: + sprintf( + buf, GS(STATS_DETAIL_FMT), ((FINAL_STATS *)stats)->found_secrets, + ((FINAL_STATS *)stats)->total_secrets); + M_Row(s, GS(STATS_SECRETS), buf); + break; + + case M_ROW_KILLS: + sprintf(buf, GS(STATS_BASIC_FMT), stats->kill_count); + M_Row(s, GS(STATS_KILLS), buf); + break; + + case M_ROW_AMMO_USED: + sprintf(buf, "%d", stats->ammo_used); + M_Row(s, GS(STATS_AMMO_USED), buf); + break; + + case M_ROW_AMMO_HITS: + sprintf(buf, "%d", stats->ammo_hits); + M_Row(s, GS(STATS_AMMO_HITS), buf); + break; + + case M_ROW_MEDIPACKS: + sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used); + M_Row(s, GS(STATS_MEDIPACKS_USED), buf); + break; + + case M_ROW_DISTANCE_TRAVELLED: + M_FormatDistance(buf, stats->distance_travelled); + M_Row(s, GS(STATS_DISTANCE_TRAVELLED), buf); + break; + + default: + break; + } +} + +static void M_LevelStatsRows(const UI_STATS_DIALOG_STATE *const s) +{ + const GF_LEVEL *const current_level = + GF_GetLevel(GFLT_MAIN, s->args.level_num); + const RESUME_INFO *const current_info = + Savegame_GetCurrentInfo(current_level); + const STATS_COMMON *const stats = (STATS_COMMON *)¤t_info->stats; + + M_RowFromRole(s, M_ROW_TIMER, stats); + if (stats->max_secret_count != 0) { + M_RowFromRole(s, M_ROW_LEVEL_SECRETS, stats); + } + M_RowFromRole(s, M_ROW_KILLS, stats); + M_RowFromRole(s, M_ROW_AMMO_USED, stats); + M_RowFromRole(s, M_ROW_AMMO_HITS, stats); + M_RowFromRole(s, M_ROW_MEDIPACKS, stats); + M_RowFromRole(s, M_ROW_DISTANCE_TRAVELLED, stats); +} + +static void M_FinalStatsRows(const UI_STATS_DIALOG_STATE *const s) +{ + const GF_LEVEL_TYPE level_type = + GF_GetLevel(GFLT_MAIN, s->args.level_num)->type; + const FINAL_STATS final_stats = Stats_ComputeFinalStats(level_type); + const STATS_COMMON *const stats = (STATS_COMMON *)&final_stats; + M_RowFromRole(s, M_ROW_TIMER, stats); + M_RowFromRole(s, M_ROW_ALL_SECRETS, stats); + M_RowFromRole(s, M_ROW_KILLS, stats); + M_RowFromRole(s, M_ROW_AMMO_USED, stats); + M_RowFromRole(s, M_ROW_AMMO_HITS, stats); + M_RowFromRole(s, M_ROW_MEDIPACKS, stats); + M_RowFromRole(s, M_ROW_DISTANCE_TRAVELLED, stats); +} + +static void M_AssaultCourseStatsRows(UI_STATS_DIALOG_STATE *const s) +{ + const ASSAULT_STATS stats = Gym_GetAssaultStats(); + UI_BeginRequester(&s->assault_req, M_GetDialogTitle(s)); + // ensure minimum dialog width + UI_Spacer(290.0f, 0.0f); + if (stats.entries[0].time == 0) { + UI_BeginAnchor(0.5f, 0.5f); + UI_Label(GS(STATS_ASSAULT_NO_TIMES_SET)); + UI_EndAnchor(); + } else { + const int32_t first = UI_Requester_GetFirstRow(&s->assault_req); + const int32_t last = UI_Requester_GetLastRow(&s->assault_req); + for (int32_t i = first; i < last; i++) { + if (stats.entries[i].time == 0) { + break; + } + + char left_buf[32] = " "; + char right_buf[32] = " "; + sprintf( + left_buf, "%2d: %s %d", i + 1, GS(STATS_ASSAULT_FINISH), + stats.entries[i].attempt_num); + + const int32_t sec = stats.entries[i].time / FRAMES_PER_SECOND; + sprintf( + right_buf, "%02d:%02d.%-2d", sec / 60, sec % 60, + stats.entries[i].time % FRAMES_PER_SECOND + / (FRAMES_PER_SECOND / 10)); + + M_Row(s, left_buf, right_buf); + } + } + UI_EndRequester(&s->assault_req); +} + +static const char *M_GetDialogTitle(const UI_STATS_DIALOG_STATE *const s) +{ + switch (s->args.mode) { + case UI_STATS_DIALOG_MODE_LEVEL: + return GF_GetLevel(GFLT_MAIN, s->args.level_num)->title; + case UI_STATS_DIALOG_MODE_FINAL: { + const GF_LEVEL_TYPE level_type = + GF_GetLevel(GFLT_MAIN, s->args.level_num)->type; + const char *const title = level_type == GFL_BONUS + ? GS(STATS_BONUS_STATISTICS) + : GS(STATS_FINAL_STATISTICS); + return title; + } + case UI_STATS_DIALOG_MODE_ASSAULT_COURSE: + return GS(STATS_ASSAULT_TITLE); + } + return nullptr; +} + +static void M_BeginDialog(const UI_STATS_DIALOG_STATE *const s) +{ + const char *const title = M_GetDialogTitle(s); + if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .spacing = { .v = 11.0f }, + .align = { .h = UI_STACK_H_ALIGN_CENTER }, + }); + if (title != nullptr) { + UI_Label(title); + } + } else { + UI_BeginWindow(); + if (title != nullptr) { + UI_WindowTitle(title); + } + UI_BeginWindowBody(); + UI_BeginStackEx((UI_STACK_SETTINGS) { + .orientation = UI_STACK_VERTICAL, + .spacing = { .v = 3.0f }, + .align = { .h = UI_STACK_H_ALIGN_SPAN }, + }); + } + // ensure minimum dialog width + UI_Spacer(290.0f, 0.0f); +} + +static void M_EndDialog(const UI_STATS_DIALOG_STATE *const s) +{ + if (s->args.style == UI_STATS_DIALOG_STYLE_BARE) { + UI_EndStack(); + } else { + UI_EndStack(); + UI_EndWindowBody(); + UI_EndWindow(); + } +} + +void UI_StatsDialog(UI_STATS_DIALOG_STATE *const s) +{ + // TODO: add support for the bare style by merging TR1 and TR2 stats dialog + // implementations. + ASSERT(s->args.style == UI_STATS_DIALOG_STYLE_BORDERED); + + UI_BeginModal(0.5f, 1.0f); + UI_BeginPad(40.f, 40.0f); + + switch (s->args.mode) { + case UI_STATS_DIALOG_MODE_LEVEL: + M_BeginDialog(s); + M_LevelStatsRows(s); + M_EndDialog(s); + break; + case UI_STATS_DIALOG_MODE_FINAL: + M_BeginDialog(s); + M_FinalStatsRows(s); + M_EndDialog(s); + break; + case UI_STATS_DIALOG_MODE_ASSAULT_COURSE: + M_AssaultCourseStatsRows(s); + break; + } + + UI_EndPad(); + UI_EndModal(); +} diff --git a/src/tr2/game/ui/dialogs/stats.h b/src/tr2/game/ui/dialogs/stats.h new file mode 100644 index 000000000..00c84b406 --- /dev/null +++ b/src/tr2/game/ui/dialogs/stats.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/src/tr2/game/ui/widgets/controls_backend_selector.c b/src/tr2/game/ui/widgets/controls_backend_selector.c deleted file mode 100644 index 7ad4b6e49..000000000 --- a/src/tr2/game/ui/widgets/controls_backend_selector.c +++ /dev/null @@ -1,111 +0,0 @@ -#include "game/ui/widgets/controls_backend_selector.h" - -#include "game/game_string.h" - -#include -#include -#include - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *keyboard_label; - UI_WIDGET *controller_label; - UI_WIDGET *container; - UI_CONTROLS_CONTROLLER *controller; -} UI_CONTROLS_BACKEND_SELECTOR; - -static void M_SyncOutlines(UI_CONTROLS_BACKEND_SELECTOR *self); - -static void M_UpdateText(UI_CONTROLS_BACKEND_SELECTOR *self); -static int32_t M_GetWidth(const UI_CONTROLS_BACKEND_SELECTOR *self); -static int32_t M_GetHeight(const UI_CONTROLS_BACKEND_SELECTOR *self); -static void M_SetPosition( - UI_CONTROLS_BACKEND_SELECTOR *self, int32_t x, int32_t y); -static void M_Control(UI_CONTROLS_BACKEND_SELECTOR *self); -static void M_Draw(UI_CONTROLS_BACKEND_SELECTOR *self); -static void M_Free(UI_CONTROLS_BACKEND_SELECTOR *self); - -static void M_SyncOutlines(UI_CONTROLS_BACKEND_SELECTOR *const self) -{ - UI_Label_RemoveFrame(self->keyboard_label); - UI_Label_RemoveFrame(self->controller_label); - switch (self->controller->backend) { - case INPUT_BACKEND_KEYBOARD: - UI_Label_AddFrame(self->keyboard_label); - break; - case INPUT_BACKEND_CONTROLLER: - UI_Label_AddFrame(self->controller_label); - break; - default: - break; - } -} - -static int32_t M_GetWidth(const UI_CONTROLS_BACKEND_SELECTOR *const self) -{ - return self->container->get_width(self->container); -} - -static int32_t M_GetHeight(const UI_CONTROLS_BACKEND_SELECTOR *const self) -{ - return self->container->get_height(self->container); -} - -static void M_SetPosition( - UI_CONTROLS_BACKEND_SELECTOR *const self, const int32_t x, const int32_t y) -{ - self->container->set_position(self->container, x, y); -} - -static void M_Control(UI_CONTROLS_BACKEND_SELECTOR *const self) -{ - M_SyncOutlines(self); -} - -static void M_Draw(UI_CONTROLS_BACKEND_SELECTOR *const self) -{ - if (self->container->draw != nullptr) { - self->container->draw(self->container); - } -} - -static void M_Free(UI_CONTROLS_BACKEND_SELECTOR *const self) -{ - self->keyboard_label->free(self->keyboard_label); - self->controller_label->free(self->controller_label); - self->container->free(self->container); - Memory_Free(self); -} - -UI_WIDGET *UI_ControlsBackendSelector_Create( - UI_CONTROLS_CONTROLLER *const controller) -{ - UI_CONTROLS_BACKEND_SELECTOR *const self = - Memory_Alloc(sizeof(UI_CONTROLS_BACKEND_SELECTOR)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->controller = controller; - - self->keyboard_label = - UI_Label_Create(GS(CONTROL_BACKEND_KEYBOARD), UI_LABEL_AUTO_SIZE, 16); - self->controller_label = - UI_Label_Create(GS(CONTROL_BACKEND_CONTROLLER), UI_LABEL_AUTO_SIZE, 16); - - self->container = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - - UI_Stack_AddChild(self->container, self->keyboard_label); - UI_Stack_AddChild(self->container, self->controller_label); - UI_Stack_SetHAlign(self->container, UI_STACK_H_ALIGN_CENTER); - - M_SyncOutlines(self); - - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/controls_backend_selector.h b/src/tr2/game/ui/widgets/controls_backend_selector.h deleted file mode 100644 index b2c085757..000000000 --- a/src/tr2/game/ui/widgets/controls_backend_selector.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "game/ui/controllers/controls.h" - -#include - -UI_WIDGET *UI_ControlsBackendSelector_Create( - UI_CONTROLS_CONTROLLER *controller); diff --git a/src/tr2/game/ui/widgets/controls_column.c b/src/tr2/game/ui/widgets/controls_column.c deleted file mode 100644 index 162d3b765..000000000 --- a/src/tr2/game/ui/widgets/controls_column.c +++ /dev/null @@ -1,87 +0,0 @@ -#include "game/ui/widgets/controls_column.h" - -#include "game/ui/widgets/controls_input_selector.h" - -#include -#include - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *container; - int32_t selector_count; - UI_WIDGET **selectors; -} UI_CONTROLS_COLUMN; - -static int32_t M_GetWidth(const UI_CONTROLS_COLUMN *self); -static int32_t M_GetHeight(const UI_CONTROLS_COLUMN *self); -static void M_SetPosition(UI_CONTROLS_COLUMN *self, int32_t x, int32_t y); -static void M_Control(UI_CONTROLS_COLUMN *self); -static void M_Draw(UI_CONTROLS_COLUMN *self); -static void M_Free(UI_CONTROLS_COLUMN *self); - -static int32_t M_GetWidth(const UI_CONTROLS_COLUMN *const self) -{ - return self->container->get_width(self->container); -} - -static int32_t M_GetHeight(const UI_CONTROLS_COLUMN *const self) -{ - return self->container->get_height(self->container); -} - -static void M_SetPosition( - UI_CONTROLS_COLUMN *const self, const int32_t x, const int32_t y) -{ - self->container->set_position(self->container, x, y); -} - -static void M_Control(UI_CONTROLS_COLUMN *const self) -{ - if (self->container->control != nullptr) { - self->container->control(self->container); - } -} - -static void M_Draw(UI_CONTROLS_COLUMN *const self) -{ - if (self->container->draw != nullptr) { - self->container->draw(self->container); - } -} - -static void M_Free(UI_CONTROLS_COLUMN *const self) -{ - for (int32_t i = 0; i < self->selector_count; i++) { - self->selectors[i]->free(self->selectors[i]); - } - self->container->free(self->container); - Memory_FreePointer(&self->selectors); - Memory_Free(self); -} - -UI_WIDGET *UI_ControlsColumn_Create( - const int32_t column, UI_CONTROLS_CONTROLLER *const controller) -{ - UI_CONTROLS_COLUMN *const self = Memory_Alloc(sizeof(UI_CONTROLS_COLUMN)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->selector_count = UI_ControlsController_GetInputRoleCount(column); - self->container = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - self->selectors = Memory_Alloc(sizeof(UI_WIDGET *) * self->selector_count); - - for (int32_t i = 0; i < self->selector_count; i++) { - self->selectors[i] = UI_ControlsInputSelector_Create( - UI_ControlsController_GetInputRole(column, i), controller); - UI_Stack_AddChild(self->container, self->selectors[i]); - } - - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/controls_column.h b/src/tr2/game/ui/widgets/controls_column.h deleted file mode 100644 index 3589dd486..000000000 --- a/src/tr2/game/ui/widgets/controls_column.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "game/ui/controllers/controls.h" - -#include - -UI_WIDGET *UI_ControlsColumn_Create( - int32_t column, UI_CONTROLS_CONTROLLER *controller); diff --git a/src/tr2/game/ui/widgets/controls_dialog.c b/src/tr2/game/ui/widgets/controls_dialog.c deleted file mode 100644 index b28bf21e1..000000000 --- a/src/tr2/game/ui/widgets/controls_dialog.c +++ /dev/null @@ -1,122 +0,0 @@ -#include "game/ui/widgets/controls_dialog.h" - -#include "game/game_string.h" -#include "game/ui/widgets/controls_backend_selector.h" -#include "game/ui/widgets/controls_column.h" -#include "game/ui/widgets/controls_layout_editor.h" - -#include -#include -#include - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_CONTROLS_CONTROLLER *controller; - UI_WIDGET *window; - UI_WIDGET *backend_selector; - UI_WIDGET *layout_editor; - int32_t listener; -} UI_CONTROLS_DIALOG; - -static void M_DoLayout(UI_CONTROLS_DIALOG *self); -static void M_HandleCanvasResize(const EVENT *event, void *data); -static int32_t M_GetWidth(const UI_CONTROLS_DIALOG *self); -static int32_t M_GetHeight(const UI_CONTROLS_DIALOG *self); -static void M_SetPosition(UI_CONTROLS_DIALOG *self, int32_t x, int32_t y); -static void M_Control(UI_CONTROLS_DIALOG *self); -static void M_Draw(UI_CONTROLS_DIALOG *self); -static void M_Free(UI_CONTROLS_DIALOG *self); - -static void M_DoLayout(UI_CONTROLS_DIALOG *const self) -{ - M_SetPosition( - self, (UI_GetCanvasWidth() - M_GetWidth(self)) / 2, - (UI_GetCanvasHeight() - M_GetHeight(self)) * 2 / 3); -} - -static void M_HandleCanvasResize(const EVENT *event, void *data) -{ - UI_CONTROLS_DIALOG *const self = (UI_CONTROLS_DIALOG *)data; - M_DoLayout(self); -} - -static int32_t M_GetWidth(const UI_CONTROLS_DIALOG *const self) -{ - return self->window->get_width(self->window); -} - -static int32_t M_GetHeight(const UI_CONTROLS_DIALOG *const self) -{ - return self->window->get_height(self->window); -} - -static void M_SetPosition( - UI_CONTROLS_DIALOG *const self, const int32_t x, const int32_t y) -{ - self->window->set_position(self->window, x, y); -} - -static void M_Control(UI_CONTROLS_DIALOG *const self) -{ - // Trigger the UI updates only if anything has changed. - if (UI_ControlsController_Control(self->controller)) { - // Set the root widget - backend selector modal or the inputs modal - if (self->controller->state == UI_CONTROLS_STATE_NAVIGATE_BACKEND - || self->controller->state == UI_CONTROLS_STATE_EXIT) { - UI_Window_SetTitle(self->window, GS(CONTROL_CUSTOMIZE)); - UI_Window_SetRootWidget(self->window, self->backend_selector); - } else { - UI_Window_SetTitle(self->window, nullptr); - UI_Window_SetRootWidget(self->window, self->layout_editor); - } - M_DoLayout(self); - - if (self->window->control != nullptr) { - self->window->control(self->window); - } - } -} - -static void M_Draw(UI_CONTROLS_DIALOG *const self) -{ - if (self->window->draw != nullptr) { - self->window->draw(self->window); - } -} - -static void M_Free(UI_CONTROLS_DIALOG *const self) -{ - self->layout_editor->free(self->layout_editor); - self->backend_selector->free(self->backend_selector); - self->window->free(self->window); - UI_Events_Unsubscribe(self->listener); - Memory_Free(self); -} - -UI_WIDGET *UI_ControlsDialog_Create(UI_CONTROLS_CONTROLLER *const controller) -{ - UI_CONTROLS_DIALOG *const self = Memory_Alloc(sizeof(UI_CONTROLS_DIALOG)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->controller = controller; - - self->layout_editor = UI_ControlsLayoutEditor_Create(self->controller); - self->backend_selector = - UI_ControlsBackendSelector_Create(self->controller); - self->window = UI_Window_Create(self->backend_selector, 5, 5, 10, 5); - - UI_Window_SetTitle(self->window, GS(CONTROL_CUSTOMIZE)); - - self->listener = UI_Events_Subscribe( - "canvas_resize", nullptr, M_HandleCanvasResize, self); - - M_DoLayout(self); - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/controls_dialog.h b/src/tr2/game/ui/widgets/controls_dialog.h deleted file mode 100644 index 8cec14bb8..000000000 --- a/src/tr2/game/ui/widgets/controls_dialog.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "game/ui/controllers/controls.h" - -#include - -UI_WIDGET *UI_ControlsDialog_Create(UI_CONTROLS_CONTROLLER *controller); diff --git a/src/tr2/game/ui/widgets/controls_input_selector.c b/src/tr2/game/ui/widgets/controls_input_selector.c deleted file mode 100644 index edc34c25c..000000000 --- a/src/tr2/game/ui/widgets/controls_input_selector.c +++ /dev/null @@ -1,135 +0,0 @@ -#include "game/ui/widgets/controls_input_selector.h" - -#include -#include -#include - -#include -#include - -typedef struct { - UI_WIDGET_VTABLE vtable; - INPUT_ROLE input_role; - UI_WIDGET *label; - UI_WIDGET *choice; - UI_WIDGET *container; - UI_CONTROLS_CONTROLLER *controller; -} UI_CONTROLS_INPUT_SELECTOR; - -static void M_UpdateText(UI_CONTROLS_INPUT_SELECTOR *self); -static int32_t M_GetWidth(const UI_CONTROLS_INPUT_SELECTOR *self); -static int32_t M_GetHeight(const UI_CONTROLS_INPUT_SELECTOR *self); -static void M_SetPosition( - UI_CONTROLS_INPUT_SELECTOR *self, int32_t x, int32_t y); -static void M_Control(UI_CONTROLS_INPUT_SELECTOR *self); -static void M_Draw(UI_CONTROLS_INPUT_SELECTOR *self); -static void M_Free(UI_CONTROLS_INPUT_SELECTOR *self); - -static void M_UpdateText(UI_CONTROLS_INPUT_SELECTOR *const self) -{ - const char *const key_name = Input_GetKeyName( - self->controller->backend, self->controller->active_layout, - self->input_role); - UI_Label_ChangeText(self->choice, key_name); - const char *const role_name = Input_GetRoleName(self->input_role); - char role_name_padded[strlen(role_name) + 2]; - sprintf(role_name_padded, "%s ", role_name); - UI_Label_ChangeText(self->label, role_name_padded); -} - -static int32_t M_GetWidth(const UI_CONTROLS_INPUT_SELECTOR *const self) -{ - return self->container->get_width(self->container); -} - -static int32_t M_GetHeight(const UI_CONTROLS_INPUT_SELECTOR *const self) -{ - return self->container->get_height(self->container); -} - -static void M_SetPosition( - UI_CONTROLS_INPUT_SELECTOR *const self, const int32_t x, const int32_t y) -{ - self->container->set_position(self->container, x, y); -} - -static void M_Control(UI_CONTROLS_INPUT_SELECTOR *const self) -{ - if (self->label->control != nullptr) { - self->label->control(self->label); - } - if (self->choice->control != nullptr) { - self->choice->control(self->choice); - } - - // Sync outlines - UI_Label_RemoveFrame(self->label); - UI_Label_RemoveFrame(self->choice); - if (self->controller->active_role == self->input_role) { - if (self->controller->state == UI_CONTROLS_STATE_NAVIGATE_INPUTS - || self->controller->state - == UI_CONTROLS_STATE_NAVIGATE_INPUTS_DEBOUNCE) { - UI_Label_AddFrame(self->label); - } else if (self->controller->state == UI_CONTROLS_STATE_LISTEN) { - UI_Label_AddFrame(self->choice); - } - } - - M_UpdateText(self); - - // Flash conflicts - UI_Label_Flash(self->choice, false, 0); - if (Input_IsKeyConflicted( - self->controller->backend, self->controller->active_layout, - self->input_role)) { - UI_Label_Flash(self->choice, true, 20); - } -} - -static void M_Draw(UI_CONTROLS_INPUT_SELECTOR *const self) -{ - if (self->label->draw != nullptr) { - self->label->draw(self->label); - } - if (self->choice->draw != nullptr) { - self->choice->draw(self->choice); - } -} - -static void M_Free(UI_CONTROLS_INPUT_SELECTOR *const self) -{ - self->label->free(self->label); - self->choice->free(self->choice); - self->container->free(self->container); - Memory_Free(self); -} - -UI_WIDGET *UI_ControlsInputSelector_Create( - const INPUT_ROLE input_role, UI_CONTROLS_CONTROLLER *const controller) -{ - UI_CONTROLS_INPUT_SELECTOR *const self = - Memory_Alloc(sizeof(UI_CONTROLS_INPUT_SELECTOR)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->controller = controller; - self->input_role = input_role; - - self->label = UI_Label_Create("", UI_LABEL_AUTO_SIZE, 15); - self->choice = UI_Label_Create("", 70, 15); - self->container = UI_Stack_Create( - UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - UI_Stack_AddChild(self->container, self->choice); - UI_Stack_AddChild(self->container, self->label); - - // update the text on init - M_UpdateText(self); - - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/controls_input_selector.h b/src/tr2/game/ui/widgets/controls_input_selector.h deleted file mode 100644 index 6c514c471..000000000 --- a/src/tr2/game/ui/widgets/controls_input_selector.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "game/ui/controllers/controls.h" - -#include - -UI_WIDGET *UI_ControlsInputSelector_Create( - INPUT_ROLE input_role, UI_CONTROLS_CONTROLLER *controller); diff --git a/src/tr2/game/ui/widgets/controls_layout_editor.c b/src/tr2/game/ui/widgets/controls_layout_editor.c deleted file mode 100644 index da5689965..000000000 --- a/src/tr2/game/ui/widgets/controls_layout_editor.c +++ /dev/null @@ -1,103 +0,0 @@ -#include "game/ui/widgets/controls_layout_editor.h" - -#include "game/ui/widgets/controls_column.h" -#include "game/ui/widgets/controls_layout_selector.h" - -#include -#include -#include - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_CONTROLS_CONTROLLER *controller; - UI_WIDGET *layout_selector; - UI_WIDGET *outer_stack; - UI_WIDGET *column_stack; - UI_WIDGET *left_column; - UI_WIDGET *right_column; -} UI_CONTROLS_LAYOUT_EDITOR; - -static void M_DoLayout(UI_CONTROLS_LAYOUT_EDITOR *self); -static int32_t M_GetWidth(const UI_CONTROLS_LAYOUT_EDITOR *self); -static int32_t M_GetHeight(const UI_CONTROLS_LAYOUT_EDITOR *self); -static void M_SetPosition( - UI_CONTROLS_LAYOUT_EDITOR *self, int32_t x, int32_t y); -static void M_Control(UI_CONTROLS_LAYOUT_EDITOR *self); -static void M_Draw(UI_CONTROLS_LAYOUT_EDITOR *self); -static void M_Free(UI_CONTROLS_LAYOUT_EDITOR *self); - -static int32_t M_GetWidth(const UI_CONTROLS_LAYOUT_EDITOR *const self) -{ - return self->outer_stack->get_width(self->outer_stack); -} - -static int32_t M_GetHeight(const UI_CONTROLS_LAYOUT_EDITOR *const self) -{ - return self->outer_stack->get_height(self->outer_stack); -} - -static void M_SetPosition( - UI_CONTROLS_LAYOUT_EDITOR *const self, const int32_t x, const int32_t y) -{ - self->outer_stack->set_position(self->outer_stack, x, y); -} - -static void M_Control(UI_CONTROLS_LAYOUT_EDITOR *const self) -{ - if (self->outer_stack->control != nullptr) { - self->outer_stack->control(self->outer_stack); - } - // Reposition the header. - UI_Stack_DoLayout(self->outer_stack); -} - -static void M_Draw(UI_CONTROLS_LAYOUT_EDITOR *const self) -{ - if (self->outer_stack->draw != nullptr) { - self->outer_stack->draw(self->outer_stack); - } -} - -static void M_Free(UI_CONTROLS_LAYOUT_EDITOR *const self) -{ - self->left_column->free(self->left_column); - self->right_column->free(self->right_column); - self->column_stack->free(self->column_stack); - self->outer_stack->free(self->outer_stack); - self->layout_selector->free(self->layout_selector); - Memory_Free(self); -} - -UI_WIDGET *UI_ControlsLayoutEditor_Create( - UI_CONTROLS_CONTROLLER *const controller) -{ - UI_CONTROLS_LAYOUT_EDITOR *const self = - Memory_Alloc(sizeof(UI_CONTROLS_LAYOUT_EDITOR)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->controller = controller; - - self->layout_selector = UI_ControlsLayoutSelector_Create(self->controller); - self->left_column = UI_ControlsColumn_Create(0, self->controller); - self->right_column = UI_ControlsColumn_Create(1, self->controller); - - self->column_stack = UI_Stack_Create( - UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - UI_Stack_AddChild(self->column_stack, self->left_column); - UI_Stack_AddChild(self->column_stack, self->right_column); - - self->outer_stack = UI_Stack_Create( - UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE); - UI_Stack_SetHAlign(self->outer_stack, UI_STACK_H_ALIGN_CENTER); - UI_Stack_AddChild(self->outer_stack, self->layout_selector); - UI_Stack_AddChild(self->outer_stack, self->column_stack); - - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/controls_layout_editor.h b/src/tr2/game/ui/widgets/controls_layout_editor.h deleted file mode 100644 index 0e3a08ea1..000000000 --- a/src/tr2/game/ui/widgets/controls_layout_editor.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "game/ui/controllers/controls.h" - -#include - -UI_WIDGET *UI_ControlsLayoutEditor_Create(UI_CONTROLS_CONTROLLER *controller); diff --git a/src/tr2/game/ui/widgets/controls_layout_selector.c b/src/tr2/game/ui/widgets/controls_layout_selector.c deleted file mode 100644 index ba956759d..000000000 --- a/src/tr2/game/ui/widgets/controls_layout_selector.c +++ /dev/null @@ -1,83 +0,0 @@ -#include "game/ui/widgets/controls_layout_selector.h" - -#include -#include - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_WIDGET *label; - UI_CONTROLS_CONTROLLER *controller; -} UI_CONTROLS_LAYOUT_SELECTOR; - -static int32_t M_GetWidth(const UI_CONTROLS_LAYOUT_SELECTOR *self); -static int32_t M_GetHeight(const UI_CONTROLS_LAYOUT_SELECTOR *self); -static void M_SetPosition( - UI_CONTROLS_LAYOUT_SELECTOR *self, int32_t x, int32_t y); -static void M_Control(UI_CONTROLS_LAYOUT_SELECTOR *self); -static void M_Draw(UI_CONTROLS_LAYOUT_SELECTOR *self); -static void M_Free(UI_CONTROLS_LAYOUT_SELECTOR *self); - -static int32_t M_GetWidth(const UI_CONTROLS_LAYOUT_SELECTOR *const self) -{ - return self->label->get_width(self->label); -} - -static int32_t M_GetHeight(const UI_CONTROLS_LAYOUT_SELECTOR *const self) -{ - return self->label->get_height(self->label); -} - -static void M_SetPosition( - UI_CONTROLS_LAYOUT_SELECTOR *const self, const int32_t x, const int32_t y) -{ - self->label->set_position(self->label, x, y); -} - -static void M_Control(UI_CONTROLS_LAYOUT_SELECTOR *const self) -{ - if (self->controller->state == UI_CONTROLS_STATE_NAVIGATE_LAYOUT) { - UI_Label_AddFrame(self->label); - UI_Label_ChangeText( - self->label, Input_GetLayoutName(self->controller->active_layout)); - } else { - UI_Label_RemoveFrame(self->label); - } - if (self->label->control != nullptr) { - self->label->control(self->label); - } -} - -static void M_Draw(UI_CONTROLS_LAYOUT_SELECTOR *const self) -{ - if (self->label->draw != nullptr) { - self->label->draw(self->label); - } -} - -static void M_Free(UI_CONTROLS_LAYOUT_SELECTOR *const self) -{ - self->label->free(self->label); - Memory_Free(self); -} - -UI_WIDGET *UI_ControlsLayoutSelector_Create( - UI_CONTROLS_CONTROLLER *const controller) -{ - UI_CONTROLS_LAYOUT_SELECTOR *self = - Memory_Alloc(sizeof(UI_CONTROLS_LAYOUT_SELECTOR)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - self->controller = controller; - self->label = UI_Label_Create( - Input_GetLayoutName(self->controller->active_layout), - UI_LABEL_AUTO_SIZE, 25); - UI_Label_AddFrame(self->label); - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/controls_layout_selector.h b/src/tr2/game/ui/widgets/controls_layout_selector.h deleted file mode 100644 index a14cead06..000000000 --- a/src/tr2/game/ui/widgets/controls_layout_selector.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "game/ui/controllers/controls.h" - -#include - -UI_WIDGET *UI_ControlsLayoutSelector_Create(UI_CONTROLS_CONTROLLER *controller); diff --git a/src/tr2/game/ui/widgets/stats_dialog.c b/src/tr2/game/ui/widgets/stats_dialog.c deleted file mode 100644 index 68815b942..000000000 --- a/src/tr2/game/ui/widgets/stats_dialog.c +++ /dev/null @@ -1,340 +0,0 @@ -#include "game/ui/widgets/stats_dialog.h" - -#include "game/game.h" -#include "game/game_flow.h" -#include "game/game_string.h" -#include "game/gym.h" -#include "game/input.h" -#include "game/stats.h" -#include "global/vars.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#define VISIBLE_ROWS 7 -#define ROW_HEIGHT 18 - -typedef enum { - M_ROW_GENERIC, - M_ROW_TIMER, - M_ROW_LEVEL_SECRETS, - M_ROW_ALL_SECRETS, - M_ROW_KILLS, - M_ROW_AMMO_USED, - M_ROW_AMMO_HITS, - M_ROW_MEDIPACKS, - M_ROW_DISTANCE_TRAVELED, -} M_ROW_ROLE; - -typedef struct { - UI_WIDGET_VTABLE vtable; - UI_STATS_DIALOG_ARGS args; - UI_WIDGET *requester; - int32_t listener; - GF_LEVEL_TYPE level_type; -} UI_STATS_DIALOG; - -static void M_AddRow( - UI_STATS_DIALOG *self, M_ROW_ROLE role, const char *left_text, - const char *right_text); -static void M_AddRowFromRole( - UI_STATS_DIALOG *self, M_ROW_ROLE role, const STATS_COMMON *stats); -static void M_AddLevelStatsRows(UI_STATS_DIALOG *self); -static void M_AddFinalStatsRows(UI_STATS_DIALOG *self); -static void M_AddAssaultCourseStatsRows(UI_STATS_DIALOG *self); -static void M_UpdateTimerRow(UI_STATS_DIALOG *self); -static void M_DoLayout(UI_STATS_DIALOG *self); -static void M_HandleLayoutUpdate(const EVENT *event, void *data); - -static int32_t M_GetWidth(const UI_STATS_DIALOG *self); -static int32_t M_GetHeight(const UI_STATS_DIALOG *self); -static void M_SetPosition(UI_STATS_DIALOG *self, int32_t x, int32_t y); -static void M_Control(UI_STATS_DIALOG *self); -static void M_Draw(UI_STATS_DIALOG *self); -static void M_Free(UI_STATS_DIALOG *self); - -static void M_AddRow( - UI_STATS_DIALOG *const self, const M_ROW_ROLE role, - const char *const left_text, const char *const right_text) -{ - UI_Requester_AddRowLR( - self->requester, left_text, right_text, (void *)(intptr_t)role); -} - -static void M_AddRowFromRole( - UI_STATS_DIALOG *const self, const M_ROW_ROLE role, - const STATS_COMMON *const stats) -{ - char buf[64]; - - switch (role) { - case M_ROW_TIMER: { - const int32_t sec = stats->timer / FRAMES_PER_SECOND; - sprintf( - buf, "%02d:%02d:%02d", (sec / 60) / 60, (sec / 60) % 60, sec % 60); - M_AddRow(self, role, GS(STATS_TIME_TAKEN), buf); - break; - } - - case M_ROW_LEVEL_SECRETS: { - char *ptr = buf; - int32_t num_secrets = 0; - const LEVEL_STATS *const level_stats = (LEVEL_STATS *)stats; - for (int32_t i = 1; i >= 0; i--) { - for (int32_t j = 0; j < 3; j++) { - const int32_t flag = 1 << (j + i * 3); - if ((level_stats->secret_flags & flag) != 0) { - sprintf(ptr, "\\{secret %d}", j + 1); - num_secrets++; - } else { - strcpy(ptr, " "); - } - ptr += strlen(ptr); - } - } - - *ptr++ = '\0'; - if (num_secrets == 0) { - strcpy(buf, GS(MISC_NONE)); - } - M_AddRow(self, role, GS(STATS_SECRETS), buf); - break; - } - - case M_ROW_ALL_SECRETS: - sprintf( - buf, GS(STATS_DETAIL_FMT), ((FINAL_STATS *)stats)->found_secrets, - ((FINAL_STATS *)stats)->total_secrets); - M_AddRow(self, role, GS(STATS_SECRETS), buf); - break; - - case M_ROW_KILLS: - sprintf(buf, GS(STATS_BASIC_FMT), stats->kills); - M_AddRow(self, role, GS(STATS_KILLS), buf); - break; - - case M_ROW_AMMO_USED: - sprintf(buf, "%d", stats->ammo_used); - M_AddRow(self, role, GS(STATS_AMMO_USED), buf); - break; - - case M_ROW_AMMO_HITS: - sprintf(buf, "%d", stats->ammo_hits); - M_AddRow(self, role, GS(STATS_AMMO_HITS), buf); - break; - - case M_ROW_MEDIPACKS: - if ((stats->medipacks & 1) != 0) { - sprintf(buf, "%d.5", stats->medipacks >> 1); - } else { - sprintf(buf, "%d.0", stats->medipacks >> 1); - } - M_AddRow(self, role, GS(STATS_MEDIPACKS_USED), buf); - break; - - case M_ROW_DISTANCE_TRAVELED: - const int32_t distance = stats->distance / 445; - if (distance < 1000) { - sprintf(buf, "%dm", distance); - } else { - sprintf(buf, "%d.%02dkm", distance / 1000, (distance % 1000) / 10); - } - M_AddRow(self, role, GS(STATS_DISTANCE_TRAVELLED), buf); - break; - - default: - break; - } -} - -static void M_AddLevelStatsRows(UI_STATS_DIALOG *const self) -{ - const GF_LEVEL *const current_level = Game_GetCurrentLevel(); - const STATS_COMMON *stats = - current_level != nullptr && self->args.level_num == current_level->num - ? (STATS_COMMON *)&g_SaveGame.current_stats - : (STATS_COMMON *)&g_SaveGame.start[self->args.level_num].stats; - M_AddRowFromRole(self, M_ROW_TIMER, stats); - if (stats->max_secret_count != 0) { - M_AddRowFromRole(self, M_ROW_LEVEL_SECRETS, stats); - } - M_AddRowFromRole(self, M_ROW_KILLS, stats); - M_AddRowFromRole(self, M_ROW_AMMO_USED, stats); - M_AddRowFromRole(self, M_ROW_AMMO_HITS, stats); - M_AddRowFromRole(self, M_ROW_MEDIPACKS, stats); - M_AddRowFromRole(self, M_ROW_DISTANCE_TRAVELED, stats); -} - -static void M_AddFinalStatsRows(UI_STATS_DIALOG *const self) -{ - const FINAL_STATS final_stats = Stats_ComputeFinalStats(self->level_type); - const STATS_COMMON *stats = (STATS_COMMON *)&final_stats; - M_AddRowFromRole(self, M_ROW_TIMER, stats); - M_AddRowFromRole(self, M_ROW_ALL_SECRETS, stats); - M_AddRowFromRole(self, M_ROW_KILLS, stats); - M_AddRowFromRole(self, M_ROW_AMMO_USED, stats); - M_AddRowFromRole(self, M_ROW_AMMO_HITS, stats); - M_AddRowFromRole(self, M_ROW_MEDIPACKS, stats); - M_AddRowFromRole(self, M_ROW_DISTANCE_TRAVELED, stats); -} - -static void M_AddAssaultCourseStatsRows(UI_STATS_DIALOG *const self) -{ - const ASSAULT_STATS stats = Gym_GetAssaultStats(); - if (stats.best_time[0] == 0) { - M_AddRow(self, M_ROW_GENERIC, GS(STATS_ASSAULT_NO_TIMES_SET), nullptr); - return; - } - - for (int32_t i = 0; i < 10; i++) { - char left_buf[32] = ""; - char right_buf[32] = ""; - if (stats.best_time[i] != 0) { - sprintf( - left_buf, "%2d: %s %d", i + 1, GS(STATS_ASSAULT_FINISH), - stats.best_finish[i]); - - const int32_t sec = stats.best_time[i] / FRAMES_PER_SECOND; - sprintf( - right_buf, "%02d:%02d.%-2d", sec / 60, sec % 60, - stats.best_time[i] % FRAMES_PER_SECOND - / (FRAMES_PER_SECOND / 10)); - } - - M_AddRow(self, M_ROW_GENERIC, left_buf, right_buf); - } -} - -static void M_UpdateTimerRow(UI_STATS_DIALOG *const self) -{ - if (self->args.mode != UI_STATS_DIALOG_MODE_LEVEL) { - return; - } - - for (int32_t i = 0; i < UI_Requester_GetRowCount(self->requester); i++) { - if ((intptr_t)UI_Requester_GetRowUserData(self->requester, i) - != M_ROW_TIMER) { - continue; - } - char buf[32]; - const int32_t sec = g_SaveGame.current_stats.timer / FRAMES_PER_SECOND; - sprintf( - buf, "%02d:%02d:%02d", (sec / 60) / 60, (sec / 60) % 60, sec % 60); - UI_Requester_ChangeRowLR( - self->requester, i, nullptr, buf, (void *)(intptr_t)M_ROW_TIMER); - return; - } -} - -static void M_DoLayout(UI_STATS_DIALOG *const self) -{ - M_SetPosition( - self, (UI_GetCanvasWidth() - M_GetWidth(self)) / 2, - (UI_GetCanvasHeight() - M_GetHeight(self)) - 50); -} - -static void M_HandleLayoutUpdate(const EVENT *event, void *data) -{ - UI_STATS_DIALOG *const self = (UI_STATS_DIALOG *)data; - M_DoLayout(self); -} - -static int32_t M_GetWidth(const UI_STATS_DIALOG *const self) -{ - return self->requester->get_width(self->requester); -} - -static int32_t M_GetHeight(const UI_STATS_DIALOG *const self) -{ - return self->requester->get_height(self->requester); -} - -static void M_SetPosition( - UI_STATS_DIALOG *const self, const int32_t x, const int32_t y) -{ - self->requester->set_position(self->requester, x, y); -} - -static void M_Control(UI_STATS_DIALOG *const self) -{ - if (self->requester->control != nullptr) { - self->requester->control(self->requester); - } - - M_UpdateTimerRow(self); -} - -static void M_Draw(UI_STATS_DIALOG *const self) -{ - if (self->requester->draw != nullptr) { - self->requester->draw(self->requester); - } -} - -static void M_Free(UI_STATS_DIALOG *const self) -{ - self->requester->free(self->requester); - UI_Events_Unsubscribe(self->listener); - Memory_Free(self); -} - -UI_WIDGET *UI_StatsDialog_Create(UI_STATS_DIALOG_ARGS args) -{ - UI_STATS_DIALOG *const self = Memory_Alloc(sizeof(UI_STATS_DIALOG)); - self->vtable = (UI_WIDGET_VTABLE) { - .get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth, - .get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight, - .set_position = (UI_WIDGET_SET_POSITION)M_SetPosition, - .control = (UI_WIDGET_CONTROL)M_Control, - .draw = (UI_WIDGET_DRAW)M_Draw, - .free = (UI_WIDGET_FREE)M_Free, - }; - - // TODO: add support for the bare style by merging TR1 and TR2 stats dialog - // implementations. - ASSERT(args.style == UI_STATS_DIALOG_STYLE_BORDERED); - - self->args = args; - self->level_type = GF_GetLevel(GFLT_MAIN, self->args.level_num)->type; - self->requester = UI_Requester_Create((UI_REQUESTER_SETTINGS) { - .is_selectable = false, - .visible_rows = VISIBLE_ROWS, - .width = 290, - .row_height = ROW_HEIGHT, - }); - - self->listener = UI_Events_Subscribe( - "layout_update", nullptr, M_HandleLayoutUpdate, self); - - switch (args.mode) { - case UI_STATS_DIALOG_MODE_LEVEL: - UI_Requester_SetTitle( - self->requester, GF_GetLevel(GFLT_MAIN, args.level_num)->title); - M_AddLevelStatsRows(self); - break; - - case UI_STATS_DIALOG_MODE_FINAL: - const char *title = self->level_type == GFL_BONUS - ? GS(STATS_BONUS_STATISTICS) - : GS(STATS_FINAL_STATISTICS); - UI_Requester_SetTitle(self->requester, title); - M_AddFinalStatsRows(self); - break; - - case UI_STATS_DIALOG_MODE_ASSAULT_COURSE: - UI_Requester_SetTitle(self->requester, GS(STATS_ASSAULT_TITLE)); - M_AddAssaultCourseStatsRows(self); - break; - } - - M_DoLayout(self); - - return (UI_WIDGET *)self; -} diff --git a/src/tr2/game/ui/widgets/stats_dialog.h b/src/tr2/game/ui/widgets/stats_dialog.h deleted file mode 100644 index 96afd51a2..000000000 --- a/src/tr2/game/ui/widgets/stats_dialog.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include diff --git a/src/tr2/game/viewport.c b/src/tr2/game/viewport.c index 275f6faed..245e1c11c 100644 --- a/src/tr2/game/viewport.c +++ b/src/tr2/game/viewport.c @@ -52,7 +52,7 @@ static void M_AlterFov(VIEWPORT *const vp) const int32_t view_angle = vp->view_angle <= 0 ? g_Config.visuals.fov * DEG_1 : vp->view_angle; const int32_t fov_width = vp->game_vars.win_height * 320 - / (g_Config.visuals.use_pcx_fov ? 200 : 240); + / (g_Config.visuals.use_psx_fov ? 200 : 240); vp->game_vars.persp = fov_width / 2 * Math_Cos(view_angle / 2) / Math_Sin(view_angle / 2); @@ -120,14 +120,7 @@ static void M_ApplyGameVars(const VIEWPORT *const vp) void Viewport_Reset(void) { - int32_t win_width; - int32_t win_height; - if (Shell_IsFullscreen()) { - win_width = Shell_GetCurrentDisplayWidth(); - win_height = Shell_GetCurrentDisplayHeight(); - } else { - SDL_GetWindowSize(g_SDLWindow, &win_width, &win_height); - } + const SHELL_SIZE size = Shell_GetCurrentSize(); VIEWPORT *const vp = &m_Viewport; switch (g_Config.rendering.aspect_mode) { @@ -138,18 +131,18 @@ void Viewport_Reset(void) vp->render_ar = 16.0 / 9.0; break; case AM_ANY: - vp->render_ar = win_width / (double)win_height; + vp->render_ar = size.w / (double)size.h; break; } - vp->width = win_width / g_Config.rendering.scaler; - vp->height = win_height / g_Config.rendering.scaler; + vp->width = size.w / g_Config.rendering.scaler; + vp->height = size.h / g_Config.rendering.scaler; if (g_Config.rendering.aspect_mode != AM_ANY) { vp->width = vp->height * vp->render_ar; } - vp->near_z = VIEW_NEAR; - vp->far_z = VIEW_FAR; + vp->near_z = Output_GetNearZ() >> W2V_SHIFT; + vp->far_z = Output_GetFarZ() >> W2V_SHIFT; // We do not update vp->view_angle on purpose, as it's managed by the game // rather than the window manager. (Think cutscenes, special cameras, etc.) @@ -171,9 +164,8 @@ void Viewport_Reset(void) M_InitGameVars(&m_Viewport); M_ApplyGameVars(&m_Viewport); - const int32_t win_border = win_height * (1.0 - g_Config.rendering.sizer); - Render_SetupDisplay( - win_border, win_width, win_height, vp->width, vp->height); + const int32_t win_border = size.h * (1.0 - g_Config.rendering.sizer); + Render_SetupDisplay(win_border, size.w, size.h, vp->width, vp->height); } const VIEWPORT *Viewport_Get(void) @@ -203,6 +195,16 @@ void Viewport_AlterFOV(const int16_t view_angle) M_ApplyGameVars(&m_Viewport); } +int32_t Viewport_GetWidth(void) +{ + return g_PhdWinWidth; +} + +int32_t Viewport_GetHeight(void) +{ + return g_PhdWinHeight; +} + int32_t Viewport_GetMaxX(void) { return g_PhdWinMaxX; diff --git a/src/tr2/global/const.h b/src/tr2/global/const.h index 6f0399b90..bca90631b 100644 --- a/src/tr2/global/const.h +++ b/src/tr2/global/const.h @@ -12,7 +12,6 @@ #define STEPUP_HEIGHT ((STEP_L * 3) / 2) // = 384 #define SLOPE_DIF 60 #define MAX_WIBBLE 2 -#define MAX_SHADE 0x300 #define LIGHT_MAP_SIZE 32 #define MAX_ROOM_LIGHT_UNIT (0x2000 / (WIBBLE_SIZE / 2)) @@ -28,13 +27,10 @@ #define MAX_PALETTES 16 #define MAX_VERTICES 0x2000 #define MAX_BOUND_ROOMS 128 -#define MAX_EFFECTS 100 #define MAX_LEVELS 24 #define MAX_LEVEL_NAME_SIZE 50 // TODO: get rid of this limit #define MAX_DEMO_FILES MAX_LEVELS #define MAX_REQUESTER_ITEMS 24 -#define MAX_SAVE_SLOTS 16 -#define MAX_SG_BUFFER_SIZE 6272 #define DEATH_WAIT (5 * 2 * FRAMES_PER_SECOND) // = 300 #define DEATH_WAIT_INPUT (2 * FRAMES_PER_SECOND) // = 60 @@ -145,51 +141,8 @@ #define SPRITE_TRANS_QUARTER 0x60000000 #define SPRITE_COLOUR(r, g, b) ((r) | ((g) << 8) | ((b) << 16)) -#define VIEW_NEAR (20 * 1) // = 20 -#define VIEW_FAR (20 * WALL_L) // = 20480 - -#define FOG_START (12 * WALL_L) // = 12288 -#define FOG_END (20 * WALL_L) // = 20480 - -#define NUM_SLOTS 5 #define PITCH_SHIFT 4 #define IDS_DX5_REQUIRED 1 #define MONK_FRIENDLY_FIRE_THRESHOLD 10 - -#define GUN_AMMO_CLIP 16 -#define GUN_AMMO_QTY (GUN_AMMO_CLIP * 2) // = 32 - -#define MAGNUM_AMMO_CLIP 20 -#define MAGNUM_AMMO_QTY (MAGNUM_AMMO_CLIP * 2) // = 40 - -#define UZI_AMMO_CLIP 40 -#define UZI_AMMO_QTY (UZI_AMMO_CLIP * 2) // = 80 - -#define M16_AMMO_CLIP 40 -#define M16_AMMO_QTY M16_AMMO_CLIP // = 40 - -#define SHOTGUN_SHELL_COUNT 2 -#define SHOTGUN_AMMO_CLIP 6 -#define SHOTGUN_AMMO_QTY (SHOTGUN_AMMO_CLIP * SHOTGUN_SHELL_COUNT) // = 12 -#define SHOTGUN_PELLET_SCATTER (DEG_1 * 20) // = 3640 - -#define HARPOON_BOLT_SPEED 150 -#define HARPOON_RECOIL 4 -#define HARPOON_AMMO_CLIP 3 -#define HARPOON_AMMO_QTY HARPOON_AMMO_CLIP // = 3 - -#define GRENADE_SPEED 200 -#define GRENADE_AMMO_CLIP 1 -#define GRENADE_AMMO_QTY (GRENADE_AMMO_CLIP * 2) // = 2 - -#if defined(PSX_VERSION) && defined(JAPAN) - #define FLARE_AMMO_BOX 8 -#else - #define FLARE_AMMO_BOX 6 -#endif -#define FLARE_AMMO_QTY FLARE_AMMO_BOX - -#define LOW_LIGHT 5120 -#define HIGH_LIGHT 4096 diff --git a/src/tr2/global/types_decomp.h b/src/tr2/global/types_decomp.h index 58fec5353..8427402b1 100644 --- a/src/tr2/global/types_decomp.h +++ b/src/tr2/global/types_decomp.h @@ -80,66 +80,6 @@ typedef enum { DRAW_COLOR_KEY = 1, } DRAW_TYPE; -typedef struct STATS_COMMON { - uint32_t timer; - uint32_t ammo_used; - uint32_t ammo_hits; - uint32_t distance; - uint16_t kills; - uint8_t medipacks; - uint16_t max_secret_count; -} STATS_COMMON; - -typedef struct { - struct STATS_COMMON; - int32_t found_secrets; - int32_t total_secrets; -} FINAL_STATS; - -typedef struct { - struct STATS_COMMON; - uint8_t secret_flags; -} LEVEL_STATS; - -typedef struct { - uint16_t pistol_ammo; - uint16_t magnum_ammo; - uint16_t uzi_ammo; - uint16_t shotgun_ammo; - uint16_t m16_ammo; - uint16_t grenade_ammo; - uint16_t harpoon_ammo; - uint8_t small_medipacks; - uint8_t large_medipacks; - uint8_t reserved1; - uint8_t flares; - uint8_t gun_status; - uint8_t gun_type; - uint16_t available: 1; // 0x01 1 - uint16_t has_pistols: 1; // 0x02 2 - uint16_t has_magnums: 1; // 0x04 4 - uint16_t has_uzis: 1; // 0x08 8 - uint16_t has_shotgun: 1; // 0x10 16 - uint16_t has_m16: 1; // 0x20 32 - uint16_t has_grenade: 1; // 0x40 64 - uint16_t has_harpoon: 1; // 0x80 128 - uint16_t pad : 8; - uint16_t reserved2; - LEVEL_STATS stats; -} START_INFO; - -typedef struct { - START_INFO *start; - LEVEL_STATS current_stats; - int16_t current_level; - bool bonus_flag; - uint8_t num_pickup[2]; - uint8_t num_puzzle[4]; - uint8_t num_key[4]; - uint16_t reserved; - char buffer[MAX_SG_BUFFER_SIZE]; -} SAVEGAME_INFO; - typedef struct { int16_t lock_angles[4]; int16_t left_angles[4]; @@ -234,56 +174,6 @@ typedef struct { int32_t table[32]; // WIBBLE_SIZE } ROOM_LIGHT_TABLE; -typedef enum { - REQ_CENTER = 0x00, - REQ_USE = 0x01, - REQ_ALIGN_LEFT = 0x02, - REQ_ALIGN_RIGHT = 0x04, - REQ_HEADING = 0x08, - REQ_BEST_TIME = 0x10, - REQ_NORMAL_TIME = 0x20, - REQ_NO_TIME = 0x40, -} REQUESTER_FLAGS; - #pragma pack(pop) -typedef struct { - uint16_t no_selector : 1; - uint16_t ready : 1; // not present in the OG - uint16_t pad : 14; - uint16_t items_count; - uint16_t selected; - uint16_t visible_count; - uint16_t line_offset; - uint16_t line_old_offset; - uint16_t pix_width; - uint16_t line_height; - int16_t x_pos; - int16_t y_pos; - int16_t z_pos; - uint16_t item_string_len; - char *pitem_strings1; - char *pitem_strings2; - uint32_t *pitem_flags1; - uint32_t *pitem_flags2; - uint32_t heading_flags1; - uint32_t heading_flags2; - uint32_t background_flags; - uint32_t moreup_flags; - uint32_t moredown_flags; - uint32_t item_flags1[24]; // MAX_REQUESTER_ITEMS - uint32_t item_flags2[24]; // MAX_REQUESTER_ITEMS - TEXTSTRING *heading_text1; - TEXTSTRING *heading_text2; - TEXTSTRING *background_text; - TEXTSTRING *moreup_text; - TEXTSTRING *moredown_text; - TEXTSTRING *item_texts1[24]; // MAX_REQUESTER_ITEMS - TEXTSTRING *item_texts2[24]; // MAX_REQUESTER_ITEMS - char heading_string1[32]; - char heading_string2[32]; - uint32_t render_width; - uint32_t render_height; -} REQUEST_INFO; - // clang-format on diff --git a/src/tr2/global/vars.c b/src/tr2/global/vars.c index 2860d3dd4..820214a79 100644 --- a/src/tr2/global/vars.c +++ b/src/tr2/global/vars.c @@ -44,23 +44,19 @@ float g_FltPersp; int16_t *g_Info3DPtr = nullptr; int32_t g_PhdWinWidth; int32_t g_PhdViewDistance; -PHD_VBUF g_PhdVBuf[1500]; +PHD_VBUF *g_PhdVBuf = nullptr; float g_FltWinRight; int32_t g_PhdWinRight; int32_t g_SurfaceCount; SORT_ITEM *g_Sort3DPtr = nullptr; -bool g_IsDemoLoaded; -bool g_IsMonkAngry; uint16_t g_SoundOptionLine; int32_t g_HealthBarTimer; int32_t g_LevelComplete; -SAVEGAME_INFO g_SaveGame = {}; LARA_INFO g_Lara; ITEM *g_LaraItem = nullptr; CREATURE *g_BaddieSlots = nullptr; bool g_CameraUnderwater; -char g_LevelFileName[256]; WEAPON_INFO g_Weapons[] = { {}, @@ -172,103 +168,17 @@ WEAPON_INFO g_Weapons[] = { }; int16_t g_FinalBossActive; -int16_t g_FinalLevelCount; +uint16_t g_FinalLevelCount; int16_t g_FinalBossCount; int16_t g_FinalBossItem[5]; static char m_LoadGameRequesterStrings1[MAX_LEVELS][50]; static char m_LoadGameRequesterStrings2[MAX_LEVELS][50]; -REQUEST_INFO g_LoadGameRequester = { - .no_selector = 0, - .ready = 0, - .pad = 0, - .items_count = 1, - .selected = 0, - .visible_count = 5, - .line_offset = 0, - .line_old_offset = 0, - .pix_width = 296, - .line_height = 18, - .x_pos = 0, - .y_pos = -32, - .z_pos = 0, - .item_string_len = 50, - .pitem_strings1 = (char *)m_LoadGameRequesterStrings1, - .pitem_strings2 = (char *)m_LoadGameRequesterStrings2, - .pitem_flags1 = nullptr, - .pitem_flags2 = nullptr, - .heading_flags1 = 0, - .heading_flags2 = 0, - .background_flags = 0, - .moreup_flags = 0, - .moredown_flags = 0, - .item_flags1 = {}, - .item_flags2 = {}, - .heading_text1 = nullptr, - .heading_text2 = nullptr, - .background_text = nullptr, - .moreup_text = nullptr, - .moredown_text = nullptr, - .item_texts1 = { nullptr }, - .item_texts2 = { nullptr }, - .heading_string1 = {}, - .heading_string2 = {}, - .render_width = 0, - .render_height = 0, -}; - -REQUEST_INFO g_SaveGameRequester = { - .no_selector = 0, - .ready = 0, - .pad = 0, - .items_count = 1, - .selected = 0, - .visible_count = 5, - .line_offset = 0, - .line_old_offset = 0, - .pix_width = 272, - .line_height = 18, - .x_pos = 0, - .y_pos = -32, - .z_pos = 0, - .item_string_len = 50, - .pitem_strings1 = (char *)g_ValidLevelStrings1, - .pitem_strings2 = (char *)g_ValidLevelStrings2, - .pitem_flags1 = nullptr, - .pitem_flags2 = nullptr, - .heading_flags1 = 0, - .heading_flags2 = 0, - .background_flags = 0, - .moreup_flags = 0, - .moredown_flags = 0, - .item_flags1 = {}, - .item_flags2 = {}, - .heading_text1 = nullptr, - .heading_text2 = nullptr, - .background_text = nullptr, - .moreup_text = nullptr, - .moredown_text = nullptr, - .item_texts1 = { nullptr }, - .item_texts2 = { nullptr }, - .heading_string1 = {}, - .heading_string2 = {}, - .render_width = 0, - .render_height = 0, -}; - bool g_GF_RemoveAmmo = false; bool g_GF_RemoveWeapons = false; int32_t g_GF_LaraStartAnim; int32_t g_GF_ScriptVersion; -int32_t g_SavedGames; -char g_ValidLevelStrings1[MAX_LEVELS][50]; -char g_ValidLevelStrings2[MAX_LEVELS][50]; -uint32_t g_RequesterFlags1[MAX_REQUESTER_ITEMS]; -uint32_t g_RequesterFlags2[MAX_REQUESTER_ITEMS]; -int32_t g_SaveCounter; -int16_t g_SavedLevels[MAX_LEVELS] = { -1, 0 }; - XYZ_32 g_InteractPosition = { .x = 0, .y = 0, .z = 0 }; bool g_DetonateAllMines = false; diff --git a/src/tr2/global/vars.h b/src/tr2/global/vars.h index c6d53f07f..bcfa815fb 100644 --- a/src/tr2/global/vars.h +++ b/src/tr2/global/vars.h @@ -45,40 +45,27 @@ extern float g_FltPersp; extern int16_t *g_Info3DPtr; extern int32_t g_PhdWinWidth; extern int32_t g_PhdViewDistance; -extern PHD_VBUF g_PhdVBuf[]; +extern PHD_VBUF *g_PhdVBuf; extern float g_FltWinRight; extern int32_t g_PhdWinRight; extern int32_t g_SurfaceCount; extern SORT_ITEM *g_Sort3DPtr; -extern bool g_IsDemoLoaded; -extern bool g_IsMonkAngry; extern uint16_t g_SoundOptionLine; extern int32_t g_HealthBarTimer; extern int32_t g_LevelComplete; -extern SAVEGAME_INFO g_SaveGame; extern LARA_INFO g_Lara; extern ITEM *g_LaraItem; extern CREATURE *g_BaddieSlots; extern bool g_CameraUnderwater; -extern char g_LevelFileName[256]; extern WEAPON_INFO g_Weapons[]; extern int16_t g_FinalBossActive; -extern int16_t g_FinalLevelCount; +extern uint16_t g_FinalLevelCount; extern int16_t g_FinalBossCount; extern int16_t g_FinalBossItem[5]; -extern REQUEST_INFO g_LoadGameRequester; -extern REQUEST_INFO g_SaveGameRequester; extern bool g_GF_RemoveAmmo; extern bool g_GF_RemoveWeapons; extern int32_t g_GF_LaraStartAnim; -extern int32_t g_SavedGames; -extern char g_ValidLevelStrings1[MAX_LEVELS][50]; -extern char g_ValidLevelStrings2[MAX_LEVELS][50]; -extern uint32_t g_RequesterFlags1[MAX_REQUESTER_ITEMS]; -extern uint32_t g_RequesterFlags2[MAX_REQUESTER_ITEMS]; -extern int32_t g_SaveCounter; -extern int16_t g_SavedLevels[MAX_LEVELS]; extern XYZ_32 g_InteractPosition; extern bool g_DetonateAllMines; diff --git a/src/tr2/meson.build b/src/tr2/meson.build index 4081b34e0..ea203d6bf 100644 --- a/src/tr2/meson.build +++ b/src/tr2/meson.build @@ -90,7 +90,6 @@ sources = [ init, 'decomp/decomp.c', 'decomp/flares.c', - 'decomp/savegame.c', 'decomp/skidoo.c', 'game/camera.c', 'game/clock.c', @@ -190,10 +189,6 @@ sources = [ 'game/objects/general/bell.c', 'game/objects/general/big_bowl.c', 'game/objects/general/bird_tweeter.c', - 'game/objects/general/bridge_common.c', - 'game/objects/general/bridge_flat.c', - 'game/objects/general/bridge_tilt_1.c', - 'game/objects/general/bridge_tilt_2.c', 'game/objects/general/camera_target.c', 'game/objects/general/clock_chimes.c', 'game/objects/general/copter.c', @@ -201,7 +196,6 @@ sources = [ 'game/objects/general/detonator.c', 'game/objects/general/ding_dong.c', 'game/objects/general/door.c', - 'game/objects/general/drawbridge.c', 'game/objects/general/earthquake.c', 'game/objects/general/final_cutscene.c', 'game/objects/general/final_level_counter.c', @@ -219,7 +213,6 @@ sources = [ 'game/objects/general/puzzle_hole.c', 'game/objects/general/sphere_of_doom.c', 'game/objects/general/switch.c', - 'game/objects/general/trapdoor.c', 'game/objects/general/waterfall.c', 'game/objects/general/window.c', 'game/objects/general/zipline.c', @@ -263,11 +256,11 @@ sources = [ 'game/render/hwr.c', 'game/render/priv.c', 'game/render/swr.c', - 'game/requester.c', 'game/room.c', 'game/room_draw.c', 'game/savegame/common.c', - 'game/scaler.c', + 'game/savegame/savegame_bson.c', + 'game/savegame/savegame_legacy.c', 'game/shell/common.c', 'game/shell/input.c', 'game/sound.c', @@ -275,14 +268,8 @@ sources = [ 'game/stats.c', 'game/text.c', 'game/ui/common.c', - 'game/ui/controllers/controls.c', - 'game/ui/widgets/controls_backend_selector.c', - 'game/ui/widgets/controls_column.c', - 'game/ui/widgets/controls_dialog.c', - 'game/ui/widgets/controls_input_selector.c', - 'game/ui/widgets/controls_layout_editor.c', - 'game/ui/widgets/controls_layout_selector.c', - 'game/ui/widgets/stats_dialog.c', + 'game/ui/dialogs/graphic_settings.c', + 'game/ui/dialogs/stats.c', 'game/viewport.c', 'global/enum_map.c', 'global/vars.c', diff --git a/tools/additional_lint b/tools/additional_lint index 1fe5f1acd..d57ea9c55 100755 --- a/tools/additional_lint +++ b/tools/additional_lint @@ -24,6 +24,8 @@ def filter_files( files: Iterable[Path], ignored_patterns: list[str] | None, debug: bool ) -> Iterable[Path]: for path in files: + if not path.exists(): + continue if path.is_dir(): continue if is_binary_file(path): diff --git a/tools/config/.gitignore b/tools/config/.gitignore index 7645b76b5..641a9bade 100644 --- a/tools/config/.gitignore +++ b/tools/config/.gitignore @@ -13,3 +13,4 @@ *.vcxproj *.filters *.pubxml +[Oo]ut/ diff --git a/tools/tr1/config/TR1X_ConfigTool/App.xaml b/tools/config/TR1X_ConfigTool/App.xaml similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/App.xaml rename to tools/config/TR1X_ConfigTool/App.xaml diff --git a/tools/tr1/config/TR1X_ConfigTool/App.xaml.cs b/tools/config/TR1X_ConfigTool/App.xaml.cs similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/App.xaml.cs rename to tools/config/TR1X_ConfigTool/App.xaml.cs diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic1.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic1.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic1.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic1.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic2.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic2.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic2.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic2.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic3.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic3.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic3.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic3.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic4.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic4.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic4.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic4.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic5.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic5.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic5.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic5.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic6.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic6.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic6.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic6.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic7.jpg b/tools/config/TR1X_ConfigTool/Resources/Graphics/graphic7.jpg similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Graphics/graphic7.jpg rename to tools/config/TR1X_ConfigTool/Resources/Graphics/graphic7.jpg diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/en-GB.json b/tools/config/TR1X_ConfigTool/Resources/Lang/en-GB.json similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Lang/en-GB.json rename to tools/config/TR1X_ConfigTool/Resources/Lang/en-GB.json diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/en.json b/tools/config/TR1X_ConfigTool/Resources/Lang/en.json similarity index 98% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Lang/en.json rename to tools/config/TR1X_ConfigTool/Resources/Lang/en.json index 3524a8fc7..80daa2023 100644 --- a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/en.json +++ b/tools/config/TR1X_ConfigTool/Resources/Lang/en.json @@ -54,11 +54,6 @@ "semi-lock": "Semi lock", "no-lock": "No lock" }, - "music_load_condition": { - "never": "Never", - "non-ambient": "Non-ambient", - "always": "Always" - }, "stat_mode": { "minimal": "Minimal", "detailed": "Detailed", @@ -152,7 +147,7 @@ }, "fix_texture_issues": { "Title": "Fix texture issues", - "Description": "Fixes original issues with missing or incorrect textures." + "Description": "Fixes original issues with missing or incorrect textures/meshes." }, "fix_animated_sprites": { "Title": "Fix sprite animations", @@ -238,7 +233,7 @@ "Title": "Stat detail mode", "Description": "Allows various levels of stat detail.\n- Minimal: shows kills, pickups, secrets, and time taken.\n- Detailed: shows all stats from minimal as well as the maximum pickup count and kill count of each level.\n- Full: shows all stats from detailed as well as ammo hits, ammo used, health packs used, and distance travelled." }, - "enable_cine": { + "enable_cutscenes": { "Title": "Enable cutscenes", "Description": "Enables cutscenes playing." }, @@ -250,9 +245,9 @@ "Title": "Enable FMVs", "Description": "Enables FMVs playing." }, - "enable_eidos_logo": { - "Title": "Enable EIDOS logo", - "Description": "Enables EIDOS logo at the game start." + "enable_legal": { + "Title": "Enable legal", + "Description": "Enables EIDOS logo, Core Design FMV and bink video codec FMV at the game start." }, "enable_loading_screens": { "Title": "Enable loading screens", diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/es.json b/tools/config/TR1X_ConfigTool/Resources/Lang/es.json similarity index 99% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Lang/es.json rename to tools/config/TR1X_ConfigTool/Resources/Lang/es.json index a947d52f2..f281ba2b8 100644 --- a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/es.json +++ b/tools/config/TR1X_ConfigTool/Resources/Lang/es.json @@ -153,7 +153,7 @@ "Title": "Consola", "Description": "Habilita la consola de desarrollo." }, - "enable_cine": { + "enable_cutscenes": { "Title": "Habilitar escenas", "Description": "Habilita la reproducción de escenas." }, @@ -197,9 +197,9 @@ "Title": "Habilitar FMV", "Description": "Habilita la reproducción de vídeos FMV." }, - "enable_eidos_logo": { - "Title": "Habilitar el logo de EIDOS", - "Description": "Habilita el logo de EIDOS al comienzo del juego." + "enable_legal": { + "Title": "Habilitar el contenido legal", + "Description": "Habilita el logo de EIDOS, el FMV de Core Design y el FMV de códec de video bink al inicio del juego." }, "enable_loading_screens": { "Title": "Habilitar pantallas de carga", diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/fr.json b/tools/config/TR1X_ConfigTool/Resources/Lang/fr.json similarity index 99% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Lang/fr.json rename to tools/config/TR1X_ConfigTool/Resources/Lang/fr.json index d2729b6cf..25a5c3811 100644 --- a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/fr.json +++ b/tools/config/TR1X_ConfigTool/Resources/Lang/fr.json @@ -249,7 +249,7 @@ "Title": "Mode détail Stat", "Description": "Permet différents niveaux de détail des statistiques.\n- Minime: indique les tués, les ramassages, les secrets et le temps passé.\n- Détaillée: indique toutes les statistiques de nu ainsi que le nombre maximum de ramassages et de tués pour chaque niveau.\n- Complet: indique toutes les statistiques de détaillé ainsi que les munitions touchées, les munitions utilisées, les trousses de santé utilisées et la distance parcourue." }, - "enable_cine": { + "enable_cutscenes": { "Title": "Activer les cutscenes", "Description": "Active les cutscenes en jeu." }, @@ -261,9 +261,9 @@ "Title": "Activer les FMV", "Description": "Active les FMV en jeu." }, - "enable_eidos_logo": { - "Title": "Activer le logo EIDOS", - "Description": "Active le logo EIDOS au début du jeu." + "enable_legal": { + "Title": "Activer le contenu légal", + "Description": "Active le logo EIDOS, la vidéo FMV de Core Design et la vidéo FMV avec le codec Bink au début du jeu." }, "enable_loading_screens": { "Title": "Activer les écrans de chargement", diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/it.json b/tools/config/TR1X_ConfigTool/Resources/Lang/it.json similarity index 97% rename from tools/tr1/config/TR1X_ConfigTool/Resources/Lang/it.json rename to tools/config/TR1X_ConfigTool/Resources/Lang/it.json index 4a48970c8..37d85b362 100644 --- a/tools/tr1/config/TR1X_ConfigTool/Resources/Lang/it.json +++ b/tools/config/TR1X_ConfigTool/Resources/Lang/it.json @@ -58,18 +58,6 @@ "semi-lock": "Parziale", "no-lock": "Nessuno" }, - "underwater_music": { - "full": "Completa", - "quiet": "Attenuata", - "full_no_ambient": "Completa ma senza suoni ambientali", - "quiet_no_ambient": "Attenuata ma senza suoni ambientali", - "none": "Assente" - }, - "music_load_condition": { - "never": "Mai", - "non-ambient": "Non ambientale", - "always": "Sempre" - }, "stat_mode": { "minimal": "Minimo", "detailed": "Dettagliato", @@ -249,7 +237,7 @@ "Title": "Modalità di visualizzazione delle statistiche", "Description": "Consente di scegliere tra vari livelli di dettaglio per la visualizzazione delle statistiche.\n- Minimo: mostra le uccisioni, gli oggetti raccolti, i segreti e il tempo impiegato per completare il livello.\n- Dettagliato: mostra tutte le statistiche della modalità 'Minimo' e, in aggiunta, il numero massimo di uccisioni e di oggetti recuperabili di ogni livello.\n- Completo: mostra tutte le statistiche della modalità 'Dettagliato' e, in aggiunta, i colpi andati a segno, le munizioni utilizzate, i kit medici adoperati e la distanza percorsa." }, - "enable_cine": { + "enable_cutscenes": { "Title": "Abilita le scene di intermezzo", "Description": "Abilita la riproduzione delle scene di intermezzo." }, @@ -261,9 +249,9 @@ "Title": "Abilita gli FMV", "Description": "Abilita la riproduzione dei filmati." }, - "enable_eidos_logo": { - "Title": "Abilita il logo EIDOS", - "Description": "Abilita il logo EIDOS all'avvio del gioco." + "enable_legal": { + "Title": "Abilita il contenuto legale", + "Description": "Abilita il logo EIDOS, il FMV di Core Design e il FMV del codec video Bink all'avvio del gioco." }, "enable_loading_screens": { "Title": "Abilita le schermate di caricamento", diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/TR1X.png b/tools/config/TR1X_ConfigTool/Resources/TR1X.png similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/TR1X.png rename to tools/config/TR1X_ConfigTool/Resources/TR1X.png diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/const.json b/tools/config/TR1X_ConfigTool/Resources/const.json similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/const.json rename to tools/config/TR1X_ConfigTool/Resources/const.json diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/icon.ico b/tools/config/TR1X_ConfigTool/Resources/icon.ico similarity index 100% rename from tools/tr1/config/TR1X_ConfigTool/Resources/icon.ico rename to tools/config/TR1X_ConfigTool/Resources/icon.ico diff --git a/tools/tr1/config/TR1X_ConfigTool/Resources/specification.json b/tools/config/TR1X_ConfigTool/Resources/specification.json similarity index 99% rename from tools/tr1/config/TR1X_ConfigTool/Resources/specification.json rename to tools/config/TR1X_ConfigTool/Resources/specification.json index 89ea2cb1c..a499416cd 100644 --- a/tools/tr1/config/TR1X_ConfigTool/Resources/specification.json +++ b/tools/config/TR1X_ConfigTool/Resources/specification.json @@ -314,7 +314,7 @@ "DefaultValue": "full" }, { - "Field": "enable_cine", + "Field": "enable_cutscenes", "DataType": "Bool", "DefaultValue": true }, @@ -329,7 +329,7 @@ "DefaultValue": true }, { - "Field": "enable_eidos_logo", + "Field": "enable_legal", "DataType": "Bool", "DefaultValue": true }, diff --git a/tools/tr1/config/TR1X_ConfigTool/TR1X_ConfigTool.csproj b/tools/config/TR1X_ConfigTool/TR1X_ConfigTool.csproj similarity index 87% rename from tools/tr1/config/TR1X_ConfigTool/TR1X_ConfigTool.csproj rename to tools/config/TR1X_ConfigTool/TR1X_ConfigTool.csproj index e6a7a199a..0cf47d537 100644 --- a/tools/tr1/config/TR1X_ConfigTool/TR1X_ConfigTool.csproj +++ b/tools/config/TR1X_ConfigTool/TR1X_ConfigTool.csproj @@ -1,17 +1,16 @@ - WinExe net6.0-windows disable + enable true - true + false true TR1X_ConfigTool True app.manifest - true true false @@ -23,10 +22,8 @@ - - - - + + @@ -65,9 +62,5 @@ - - - ..\..\..\config\TRX_ConfigToolLib\bin\Release\publish\TRX_ConfigToolLib.dll - diff --git a/tools/tr1/config/TR1X_ConfigTool/app.manifest b/tools/config/TR1X_ConfigTool/app.manifest similarity index 94% rename from tools/tr1/config/TR1X_ConfigTool/app.manifest rename to tools/config/TR1X_ConfigTool/app.manifest index ce145c9c0..c9bc2e2ff 100644 --- a/tools/tr1/config/TR1X_ConfigTool/app.manifest +++ b/tools/config/TR1X_ConfigTool/app.manifest @@ -10,8 +10,7 @@ version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" - language="*" - /> + language="*"/> diff --git a/tools/tr2/config/TR2X_ConfigTool/App.xaml b/tools/config/TR2X_ConfigTool/App.xaml similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/App.xaml rename to tools/config/TR2X_ConfigTool/App.xaml diff --git a/tools/tr2/config/TR2X_ConfigTool/App.xaml.cs b/tools/config/TR2X_ConfigTool/App.xaml.cs similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/App.xaml.cs rename to tools/config/TR2X_ConfigTool/App.xaml.cs diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic1.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic1.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic1.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic1.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic2.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic2.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic2.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic2.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic3.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic3.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic3.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic3.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic4.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic4.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic4.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic4.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic5.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic5.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic5.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic5.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic6.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic6.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic6.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic6.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic7.jpg b/tools/config/TR2X_ConfigTool/Resources/Graphics/graphic7.jpg similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Graphics/graphic7.jpg rename to tools/config/TR2X_ConfigTool/Resources/Graphics/graphic7.jpg diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json b/tools/config/TR2X_ConfigTool/Resources/Lang/en.json similarity index 81% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json rename to tools/config/TR2X_ConfigTool/Resources/Lang/en.json index 5b7e7f7ba..f3f2e01be 100644 --- a/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json +++ b/tools/config/TR2X_ConfigTool/Resources/Lang/en.json @@ -6,17 +6,6 @@ "window_title_about": "About TR2X Config Tool", "label_about_details": "Visit the TR2X GitHub page for further information." }, - "Enums": { - "fps": { - "30": "30", - "60": "60" - }, - "aspect_mode": { - "any": "Any", - "16:9": "16:9", - "4:3": "4:3" - } - }, "Properties": { "enable_tr3_sidesteps": { "Title": "Enhanced sidesteps", @@ -26,10 +15,6 @@ "Title": "Enable more responsive passport", "Description": "Disables blocking user input when passport flips pages, scheduling the page flips instead." }, - "fps": { - "Title": "FPS", - "Description": "Controls the framerate at which to render the game." - }, "enable_3d_pickups": { "Title": "3D pickups", "Description": "Enables 3D models to be rendered in place of the sprites for pickup items." @@ -46,6 +31,10 @@ "Title": "Enable FMVs", "Description": "Enables FMVs playing." }, + "enable_game_modes": { + "Title": "Enable game modes", + "Description": "Allows new game plus options to be selected from the new game passport menu.\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." + }, "fix_m16_accuracy": { "Title": "Fix M16 accuracy", "Description": "Fixes the accuracy of the M16 while Lara is running." @@ -78,10 +67,18 @@ "Title": "Fix walk-run-jump breaking", "Description": "Fixes Lara at times not being able to jump immediately after going from her walking to running animation." }, + "fix_bridge_collision": { + "Title": "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." + }, "fix_floor_data_issues": { "Title": "Fix floor data issues", "Description": "Fixes original issues with floor data/triggers." }, + "fix_texture_issues": { + "Title": "Fix texture issues", + "Description": "Fixes original issues with missing or incorrect textures/meshes." + }, "fix_item_rots": { "Title": "Fix item rotation issues", "Description": "Fixes original issues with some incorrectly rotated pickups when using the 3D pickups option." @@ -90,10 +87,6 @@ "Title": "Key item pre-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." }, - "aspect_mode": { - "Title": "Aspect mode", - "Description": "Set the aspect ratio of the game output." - }, "screenshot_format": { "Title": "Screenshot format", "Description": "Screenshot file format." @@ -102,6 +95,10 @@ "Title": "Gun/explosion lighting", "Description": "Enables dynamic lighting to be generated for gunshots and explosions." }, + "music_load_condition": { + "Title": "Load music track", + "Description": "Loads the music track that was playing when the game was saved.\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." + }, "enable_lara_mic" : { "Title": "Microphone at Lara", "Description": "Set the microphone to be at Lara's position. If disabled, the microphone will be at the camera's position." diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/it.json b/tools/config/TR2X_ConfigTool/Resources/Lang/it.json similarity index 81% rename from tools/tr2/config/TR2X_ConfigTool/Resources/Lang/it.json rename to tools/config/TR2X_ConfigTool/Resources/Lang/it.json index 2519c715a..28aa606a9 100644 --- a/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/it.json +++ b/tools/config/TR2X_ConfigTool/Resources/Lang/it.json @@ -6,24 +6,6 @@ "window_title_about": "Informazioni sul Programma di configurazione di TR2X", "label_about_details": "Per ulteriori informazioni, visita la pagina GitHub di TR2X." }, - "Enums": { - "fps": { - "30": "30", - "60": "60" - }, - "aspect_mode": { - "any": "Qualsiasi", - "16:9": "16:9", - "4:3": "4:3" - }, - "underwater_music": { - "full": "Completa", - "quiet": "Attenuata", - "full_no_ambient": "Completa ma senza suoni ambientali", - "quiet_no_ambient": "Attenuata ma senza suoni ambientali", - "none": "Assente" - } - }, "Properties": { "enable_tr3_sidesteps": { "Title": "Passi laterali migliorati", @@ -33,10 +15,6 @@ "Title": "Abilita passaporto reattivo", "Description": "Disabilita il blocco dell'input dell'utente mentre scorrono le pagine del passaporto, programmando invece le pagine da sfogliare." }, - "fps": { - "Title": "FPS", - "Description": "Controlla i fotogrammi al secondo a cui eseguire il gioco." - }, "enable_3d_pickups": { "Title": "Oggetti 3D", "Description": "Sostituisce gli oggetti recuperabili 2D con i rispettivi modelli 3D." @@ -53,6 +31,10 @@ "Title": "Abilita FMV", "Description": "Abilita la riproduzione dei filmati." }, + "enable_game_modes": { + "Title": "Abilita le modalità di gioco", + "Description": "Consente di selezionare diverse modalità di gioco dal menu passaporto.\n- Nuova Partita+: sblocca tutte le armi con munizioni infinite; i nemici hanno punti vita doppi.\n- NP giapponese: le armi infliggono danni doppi, i razzi di segnalazione contengono 8 anziché 6.\n- NP giapponese+: combinazione di Nuova Partita+ e NP giapponese." + }, "fix_m16_accuracy": { "Title": "Correggi precisione M16", "Description": "Corregge la precisione della mira dell'M16 quando Lara corre." @@ -85,10 +67,18 @@ "Title": "Correggi pausa camminata-corsa-salto", "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." }, + "fix_bridge_collision": { + "Title": "Correggi la collisione dei ponti", + "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." + }, "fix_floor_data_issues": { "Title": "Correggi i dati dei livelli", "Description": "Risolve i problemi dei livelli originali relativi a dati/eventi." }, + "fix_texture_issues": { + "Title": "Correggi i problemi delle texture", + "Description": "Risolve i problemi riguardanti texture mancanti o errate nei livelli originali." + }, "fix_item_rots": { "Title": "Correggi l'orientamento degli oggetti", "Description": "Risolve i problemi relativi all'orientamento errato di alcuni oggetti quando viene utilizzata l'opzione Oggetti 3D nei livelli originali." @@ -97,10 +87,6 @@ "Title": "Preselezione degli oggetti chiave", "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." }, - "aspect_mode": { - "Title": "Modalità rapporto d'aspetto", - "Description": "Imposta il rapporto d'aspetto del gioco." - }, "screenshot_format": { "Title": "Formato istantanea dello schermo", "Description": "Formato del file da utilizzare per le istantanee dello schermo." @@ -109,6 +95,10 @@ "Title": "Illuminazione spari/esplosioni", "Description": "Abilita l'illuminazione dinamica per spari ed esplosioni." }, + "music_load_condition": { + "Title": "Carica la traccia musicale", + "Description": "Carica la traccia musicale che era in riproduzione al momento del salvataggio della partita.\n- Mai: non ripristina le tracce musicali al caricamento.\n- Non ambientale: ripristina le tracce musicali ma non le tracce ambientali al caricamento.\n- Sempre: ripristina qualsiasi tipo di traccia musicale al caricamento." + }, "enable_lara_mic" : { "Title": "Microfono su Lara", "Description": "Imposta il microfono nella posizione in cui si trova Lara. Se disabilitato, il microfono sarà nella posizione della telecamera." diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/TR2X.png b/tools/config/TR2X_ConfigTool/Resources/TR2X.png similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/TR2X.png rename to tools/config/TR2X_ConfigTool/Resources/TR2X.png diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/const.json b/tools/config/TR2X_ConfigTool/Resources/const.json similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/const.json rename to tools/config/TR2X_ConfigTool/Resources/const.json diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/icon.ico b/tools/config/TR2X_ConfigTool/Resources/icon.ico similarity index 100% rename from tools/tr2/config/TR2X_ConfigTool/Resources/icon.ico rename to tools/config/TR2X_ConfigTool/Resources/icon.ico diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/specification.json b/tools/config/TR2X_ConfigTool/Resources/specification.json similarity index 87% rename from tools/tr2/config/TR2X_ConfigTool/Resources/specification.json rename to tools/config/TR2X_ConfigTool/Resources/specification.json index 0c5067a67..e5f4a5b07 100644 --- a/tools/tr2/config/TR2X_ConfigTool/Resources/specification.json +++ b/tools/config/TR2X_ConfigTool/Resources/specification.json @@ -4,21 +4,17 @@ "jpg", "png" ], - "fps": [ - "30", - "60" - ], - "aspect_mode": [ - "any", - "16:9", - "4:3" - ], "underwater_music": [ "full", "quiet", "full_no_ambient", "quiet_no_ambient", "none" + ], + "music_load_condition": [ + "never", + "non-ambient", + "always" ] }, "CategorisedProperties": [ @@ -62,11 +58,21 @@ "DataType": "Bool", "DefaultValue": true }, + { + "Field": "fix_bridge_collision", + "DataType": "Bool", + "DefaultValue": true + }, { "Field": "fix_floor_data_issues", "DataType": "Bool", "DefaultValue": true }, + { + "Field": "fix_texture_issues", + "DataType": "Bool", + "DefaultValue": true + }, { "Field": "fix_item_rots", "DataType": "Bool", @@ -128,6 +134,11 @@ "Field": "enable_fmv", "DataType": "Bool", "DefaultValue": true + }, + { + "Field": "enable_game_modes", + "DataType": "Bool", + "DefaultValue": true } ] }, @@ -140,18 +151,6 @@ "DataType": "Bool", "DefaultValue": true }, - { - "Field": "fps", - "DataType": "Enum", - "EnumKey": "fps", - "DefaultValue": "60" - }, - { - "Field": "aspect_mode", - "DataType": "Enum", - "EnumKey": "aspect_mode", - "DefaultValue": "any" - }, { "Field": "screenshot_format", "DataType": "Enum", @@ -174,6 +173,12 @@ "DataType": "Bool", "DefaultValue": false }, + { + "Field": "music_load_condition", + "DataType": "Enum", + "EnumKey": "music_load_condition", + "DefaultValue": "non-ambient" + }, { "Field": "underwater_music_mode", "DataType": "Enum", diff --git a/tools/tr2/config/TR2X_ConfigTool/TR2X_ConfigTool.csproj b/tools/config/TR2X_ConfigTool/TR2X_ConfigTool.csproj similarity index 86% rename from tools/tr2/config/TR2X_ConfigTool/TR2X_ConfigTool.csproj rename to tools/config/TR2X_ConfigTool/TR2X_ConfigTool.csproj index 60a418b0c..f3a5ac222 100644 --- a/tools/tr2/config/TR2X_ConfigTool/TR2X_ConfigTool.csproj +++ b/tools/config/TR2X_ConfigTool/TR2X_ConfigTool.csproj @@ -1,17 +1,16 @@ - WinExe net6.0-windows disable + enable true - true + false true TR2X_ConfigTool True app.manifest - true true false @@ -23,10 +22,8 @@ - - - - + + @@ -59,9 +56,5 @@ - - - ..\..\..\config\TRX_ConfigToolLib\bin\Release\publish\TRX_ConfigToolLib.dll - diff --git a/tools/tr2/config/TR2X_ConfigTool/app.manifest b/tools/config/TR2X_ConfigTool/app.manifest similarity index 94% rename from tools/tr2/config/TR2X_ConfigTool/app.manifest rename to tools/config/TR2X_ConfigTool/app.manifest index 11e012ec9..403326a7d 100644 --- a/tools/tr2/config/TR2X_ConfigTool/app.manifest +++ b/tools/config/TR2X_ConfigTool/app.manifest @@ -10,8 +10,7 @@ version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" - language="*" - /> + language="*"/> diff --git a/tools/config/TRX_ConfigToolLib.sln b/tools/config/TRX_ConfigTool.sln similarity index 53% rename from tools/config/TRX_ConfigToolLib.sln rename to tools/config/TRX_ConfigTool.sln index efff011e3..c756d9111 100644 --- a/tools/config/TRX_ConfigToolLib.sln +++ b/tools/config/TRX_ConfigTool.sln @@ -5,6 +5,10 @@ VisualStudioVersion = 17.11.35219.272 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TRX_ConfigToolLib", "TRX_ConfigToolLib\TRX_ConfigToolLib.csproj", "{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TR1X_ConfigTool", "TR1X_ConfigTool\TR1X_ConfigTool.csproj", "{A19A8ED1-D703-4954-B0F2-8E651453EB3E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TR2X_ConfigTool", "TR2X_ConfigTool\TR2X_ConfigTool.csproj", "{9FAE4BC6-08E0-46AB-9E29-6FED79D60733}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +19,14 @@ Global {27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Debug|Any CPU.Build.0 = Debug|Any CPU {27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Release|Any CPU.ActiveCfg = Release|Any CPU {27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Release|Any CPU.Build.0 = Release|Any CPU + {A19A8ED1-D703-4954-B0F2-8E651453EB3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A19A8ED1-D703-4954-B0F2-8E651453EB3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A19A8ED1-D703-4954-B0F2-8E651453EB3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A19A8ED1-D703-4954-B0F2-8E651453EB3E}.Release|Any CPU.Build.0 = Release|Any CPU + {9FAE4BC6-08E0-46AB-9E29-6FED79D60733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FAE4BC6-08E0-46AB-9E29-6FED79D60733}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FAE4BC6-08E0-46AB-9E29-6FED79D60733}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FAE4BC6-08E0-46AB-9E29-6FED79D60733}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tools/config/TRX_ConfigToolLib/Controls/AboutWindow.xaml b/tools/config/TRX_ConfigToolLib/Controls/AboutWindow.xaml index 888098dec..73f1b237a 100644 --- a/tools/config/TRX_ConfigToolLib/Controls/AboutWindow.xaml +++ b/tools/config/TRX_ConfigToolLib/Controls/AboutWindow.xaml @@ -4,7 +4,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="clr-namespace:TRX_ConfigToolLib.Models" mc:Ignorable="d" + d:DataContext="{d:DesignInstance Type=models:AboutWindowViewModel}" Title="{Binding ViewText[window_title_about]}" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterOwner" diff --git a/tools/config/TRX_ConfigToolLib/Controls/CategoryControl.xaml b/tools/config/TRX_ConfigToolLib/Controls/CategoryControl.xaml index c5619156b..f8449e4e4 100644 --- a/tools/config/TRX_ConfigToolLib/Controls/CategoryControl.xaml +++ b/tools/config/TRX_ConfigToolLib/Controls/CategoryControl.xaml @@ -5,7 +5,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:TRX_ConfigToolLib.Controls" + xmlns:models="clr-namespace:TRX_ConfigToolLib.Models" mc:Ignorable="d" + d:DataContext="{d:DesignInstance Type=models:CategoryViewModel}" d:DesignHeight="450" d:DesignWidth="800"> diff --git a/tools/config/TRX_ConfigToolLib/Controls/PropertyControl.xaml b/tools/config/TRX_ConfigToolLib/Controls/PropertyControl.xaml index 4e6914d52..dec47a92b 100644 --- a/tools/config/TRX_ConfigToolLib/Controls/PropertyControl.xaml +++ b/tools/config/TRX_ConfigToolLib/Controls/PropertyControl.xaml @@ -5,9 +5,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:TRX_ConfigToolLib.Controls" - xmlns:models="clr-namespace:TRX_ConfigToolLib.Models" - xmlns:utils="clr-namespace:TRX_ConfigToolLib.Utils" + xmlns:models="clr-namespace:TRX_ConfigToolLib.Models.Specification" + xmlns:types="clr-namespace:TRX_ConfigToolLib.Models.Specification.Types" + xmlns:converters="clr-namespace:TRX_ConfigToolLib.Utils.Converters" mc:Ignorable="d" + d:DataContext="{d:DesignInstance Type=models:BaseProperty}" d:DesignHeight="450" d:DesignWidth="800"> @@ -16,11 +18,11 @@ - - @@ -43,14 +45,14 @@ HorizontalAlignment="Left" Visibility="{Binding IsEnabled, Converter={StaticResource BoolToCollapsedConverter}}"> - + - + - + - - - @@ -126,7 +129,7 @@ Grid.Column="1" BorderBrush="#666" BorderThickness="1" - Background="{Binding SearchFailStatus, Converter={utils:ConditionalMarkupConverter TrueValue='#FFC7CE', FalseValue='White'}}"> + Background="{Binding SearchFailStatus, Converter={converters:ConditionalMarkupConverter TrueValue='#FFC7CE', FalseValue='White'}}"> @@ -201,8 +204,8 @@ Visibility="{Binding IsEditorActive, Converter={StaticResource BoolToHiddenConverter}}"/> diff --git a/tools/config/TRX_ConfigToolLib/Models/BaseLanguageViewModel.cs b/tools/config/TRX_ConfigToolLib/Models/BaseLanguageViewModel.cs index cff2ea8d5..385e3aa27 100644 --- a/tools/config/TRX_ConfigToolLib/Models/BaseLanguageViewModel.cs +++ b/tools/config/TRX_ConfigToolLib/Models/BaseLanguageViewModel.cs @@ -1,4 +1,7 @@ -namespace TRX_ConfigToolLib.Models; +using TRX_ConfigToolLib.Models.Lang; +using TRX_ConfigToolLib.Utils; + +namespace TRX_ConfigToolLib.Models; public class BaseLanguageViewModel : BaseNotifyPropertyChanged { diff --git a/tools/config/TRX_ConfigToolLib/Models/CategoryViewModel.cs b/tools/config/TRX_ConfigToolLib/Models/CategoryViewModel.cs index fc56a2867..94dac58d1 100644 --- a/tools/config/TRX_ConfigToolLib/Models/CategoryViewModel.cs +++ b/tools/config/TRX_ConfigToolLib/Models/CategoryViewModel.cs @@ -1,4 +1,5 @@ -using TRX_ConfigToolLib.Utils; +using TRX_ConfigToolLib.Models.Specification; +using TRX_ConfigToolLib.Utils; namespace TRX_ConfigToolLib.Models; diff --git a/tools/config/TRX_ConfigToolLib/Models/Lang/Language.cs b/tools/config/TRX_ConfigToolLib/Models/Lang/Language.cs index 2c44763b3..1b8e6139d 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Lang/Language.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Lang/Language.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Linq; using System.Globalization; using TRX_ConfigToolLib.Utils; -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Lang; public class Language { diff --git a/tools/config/TRX_ConfigToolLib/Models/Lang/PropertyText.cs b/tools/config/TRX_ConfigToolLib/Models/Lang/PropertyText.cs index c11a006c2..3f300118f 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Lang/PropertyText.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Lang/PropertyText.cs @@ -1,4 +1,4 @@ -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Lang; public class PropertyText { diff --git a/tools/config/TRX_ConfigToolLib/Models/MainWindowViewModel.cs b/tools/config/TRX_ConfigToolLib/Models/MainWindowViewModel.cs index 45266f259..e93e9d8f5 100644 --- a/tools/config/TRX_ConfigToolLib/Models/MainWindowViewModel.cs +++ b/tools/config/TRX_ConfigToolLib/Models/MainWindowViewModel.cs @@ -4,6 +4,7 @@ using System.IO; using System.Windows; using System.Windows.Input; using TRX_ConfigToolLib.Controls; +using TRX_ConfigToolLib.Models.Specification; using TRX_ConfigToolLib.Utils; namespace TRX_ConfigToolLib.Models; diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/BaseProperty.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/BaseProperty.cs index 2b4b2a2fe..79f671dd6 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/BaseProperty.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/BaseProperty.cs @@ -1,6 +1,7 @@ -using TRX_ConfigToolLib.Utils; +using TRX_ConfigToolLib.Models.Lang; +using TRX_ConfigToolLib.Utils; -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification; public abstract class BaseProperty : BaseNotifyPropertyChanged { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/Category.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/Category.cs index 317cb502e..3600a78d6 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/Category.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/Category.cs @@ -1,4 +1,6 @@ -namespace TRX_ConfigToolLib.Models; +using TRX_ConfigToolLib.Models.Lang; + +namespace TRX_ConfigToolLib.Models.Specification; public class Category { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/Configuration.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/Configuration.cs index f9ca4e30a..202b323b8 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/Configuration.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/Configuration.cs @@ -2,8 +2,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IO; using TRX_ConfigToolLib.Utils; +using TRX_ConfigToolLib.Utils.Json; -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification; public class Configuration { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/DataType.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/DataType.cs index 3766f58ee..d807329ee 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/DataType.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/DataType.cs @@ -1,4 +1,4 @@ -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification; public enum DataType { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/EnumOption.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/EnumOption.cs index 5b8ed1107..f80eaad37 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/EnumOption.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/EnumOption.cs @@ -1,4 +1,6 @@ -namespace TRX_ConfigToolLib.Models; +using TRX_ConfigToolLib.Models.Lang; + +namespace TRX_ConfigToolLib.Models.Specification; public class EnumOption { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/Specification.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/Specification.cs index 43da7c44f..2a6ba7af5 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/Specification.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/Specification.cs @@ -1,8 +1,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using TRX_ConfigToolLib.Utils; +using TRX_ConfigToolLib.Utils.Json; -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification; public class Specification { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/Types/BoolProperty.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/Types/BoolProperty.cs index 32ed4b4aa..3b7bc6eda 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/Types/BoolProperty.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/Types/BoolProperty.cs @@ -1,4 +1,4 @@ -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification.Types; public class BoolProperty : BaseProperty { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/Types/EnumProperty.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/Types/EnumProperty.cs index 4da283292..45b105f03 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/Types/EnumProperty.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/Types/EnumProperty.cs @@ -1,4 +1,4 @@ -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification.Types; public class EnumProperty : BaseProperty { diff --git a/tools/config/TRX_ConfigToolLib/Models/Specification/Types/NumericProperty.cs b/tools/config/TRX_ConfigToolLib/Models/Specification/Types/NumericProperty.cs index 37422a000..4c4e15344 100644 --- a/tools/config/TRX_ConfigToolLib/Models/Specification/Types/NumericProperty.cs +++ b/tools/config/TRX_ConfigToolLib/Models/Specification/Types/NumericProperty.cs @@ -1,4 +1,4 @@ -namespace TRX_ConfigToolLib.Models; +namespace TRX_ConfigToolLib.Models.Specification.Types; public class NumericProperty : BaseProperty { diff --git a/tools/config/TRX_ConfigToolLib/Resources/Lang/en.json b/tools/config/TRX_ConfigToolLib/Resources/Lang/en.json index bdfc136a1..5efe27e19 100644 --- a/tools/config/TRX_ConfigToolLib/Resources/Lang/en.json +++ b/tools/config/TRX_ConfigToolLib/Resources/Lang/en.json @@ -44,6 +44,11 @@ "jpg": "JPEG", "png": "PNG" }, + "music_load_condition": { + "never": "Never", + "non-ambient": "Non-ambient", + "always": "Always" + }, "underwater_music": { "full": "Full", "quiet": "Quiet", diff --git a/tools/config/TRX_ConfigToolLib/Resources/Lang/it.json b/tools/config/TRX_ConfigToolLib/Resources/Lang/it.json index bf5a3dd09..7cce646e4 100644 --- a/tools/config/TRX_ConfigToolLib/Resources/Lang/it.json +++ b/tools/config/TRX_ConfigToolLib/Resources/Lang/it.json @@ -38,5 +38,19 @@ "graphics": "Grafica", "sound": "Suono", "ui": "Interfaccia utente" + }, + "Enums": { + "underwater_music": { + "full": "Completa", + "quiet": "Attenuata", + "full_no_ambient": "Completa ma senza suoni ambientali", + "quiet_no_ambient": "Attenuata ma senza suoni ambientali", + "none": "Assente" + }, + "music_load_condition": { + "never": "Mai", + "non-ambient": "Non ambientale", + "always": "Sempre" + } } } diff --git a/tools/config/TRX_ConfigToolLib/TRX_ConfigToolLib.csproj b/tools/config/TRX_ConfigToolLib/TRX_ConfigToolLib.csproj index 97545b5a8..42fc35259 100644 --- a/tools/config/TRX_ConfigToolLib/TRX_ConfigToolLib.csproj +++ b/tools/config/TRX_ConfigToolLib/TRX_ConfigToolLib.csproj @@ -5,6 +5,7 @@ true enable false + true diff --git a/tools/config/TRX_ConfigToolLib/Utils/BaseNotifyPropertyChanged.cs b/tools/config/TRX_ConfigToolLib/Utils/BaseNotifyPropertyChanged.cs index 3bdaa5c1f..c5eef5ecc 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/BaseNotifyPropertyChanged.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/BaseNotifyPropertyChanged.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; -namespace TRX_ConfigToolLib; +namespace TRX_ConfigToolLib.Utils; public abstract class BaseNotifyPropertyChanged : INotifyPropertyChanged { diff --git a/tools/config/TRX_ConfigToolLib/Utils/Converters/BoolToVisibilityConverter.cs b/tools/config/TRX_ConfigToolLib/Utils/Converters/BoolToVisibilityConverter.cs index 2bf988719..a443fba96 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/Converters/BoolToVisibilityConverter.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/Converters/BoolToVisibilityConverter.cs @@ -2,7 +2,7 @@ using System.Windows; using System.Windows.Data; -namespace TRX_ConfigToolLib.Utils; +namespace TRX_ConfigToolLib.Utils.Converters; [ValueConversion(typeof(bool), typeof(Visibility))] public class BoolToVisibilityConverter : IValueConverter diff --git a/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalMarkupConverter.cs b/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalMarkupConverter.cs index 2ec09526a..847a691bf 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalMarkupConverter.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalMarkupConverter.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Windows.Data; using System.Windows.Markup; -namespace TRX_ConfigToolLib.Utils; +namespace TRX_ConfigToolLib.Utils.Converters; public class ConditionalMarkupConverter : MarkupExtension, IValueConverter { diff --git a/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalViewTextConverter.cs b/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalViewTextConverter.cs index f350b5c0f..da0348b87 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalViewTextConverter.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/Converters/ConditionalViewTextConverter.cs @@ -1,7 +1,7 @@ using System.Globalization; -using TRX_ConfigToolLib.Models; +using TRX_ConfigToolLib.Models.Lang; -namespace TRX_ConfigToolLib.Utils; +namespace TRX_ConfigToolLib.Utils.Converters; public class ConditionalViewTextConverter : ConditionalMarkupConverter { diff --git a/tools/config/TRX_ConfigToolLib/Utils/Json/NumericConverter.cs b/tools/config/TRX_ConfigToolLib/Utils/Json/NumericConverter.cs index f0a288592..a386315a7 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/Json/NumericConverter.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/Json/NumericConverter.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using System.Globalization; -namespace TRX_ConfigToolLib.Utils; +namespace TRX_ConfigToolLib.Utils.Json; public class NumericConverter : JsonConverter { diff --git a/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyConverter.cs b/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyConverter.cs index 53b87d08c..084a6655e 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyConverter.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyConverter.cs @@ -1,8 +1,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using TRX_ConfigToolLib.Models; +using TRX_ConfigToolLib.Models.Specification; +using TRX_ConfigToolLib.Models.Specification.Types; -namespace TRX_ConfigToolLib.Utils; +namespace TRX_ConfigToolLib.Utils.Json; public class PropertyConverter : JsonConverter { diff --git a/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyResolver.cs b/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyResolver.cs index 038cacaca..cb0f05469 100644 --- a/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyResolver.cs +++ b/tools/config/TRX_ConfigToolLib/Utils/Json/PropertyResolver.cs @@ -1,8 +1,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using TRX_ConfigToolLib.Models; +using TRX_ConfigToolLib.Models.Specification; -namespace TRX_ConfigToolLib.Utils; +namespace TRX_ConfigToolLib.Utils.Json; public class PropertyResolver : DefaultContractResolver { diff --git a/tools/download_assets b/tools/download_assets new file mode 100755 index 000000000..ecc2292c1 --- /dev/null +++ b/tools/download_assets @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import argparse +import shutil +import sys +import tempfile +from pathlib import Path +from urllib.request import Request, urlopen +from zipfile import ZipFile + +from shared.paths import PROJECT_PATHS + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Downloads large binary assets into local " + "data/*/ship/ directories." + ) + ) + parser.add_argument( + "game_version", choices=["1", "2", "all"], default="all", nargs="?" + ) + return parser.parse_args() + + +def download_to_file(url: str, path: Path) -> None: + print(f"Downloading {url}...") + req = Request(url, headers={"User-Agent": "download_assets"}) + with urlopen(req) as response: + if getattr(response, "status", None) not in (None, 200): + sys.exit( + f"Error: failed to download {url}. Status: {response.status}" + ) + with path.open("wb") as f: + shutil.copyfileobj(response, f) + + +def extract_zip(zip_path: Path, dest_dir: Path) -> None: + print(f"Extracting {zip_path} to {dest_dir}...") + dest_dir.mkdir(parents=True, exist_ok=True) + with ZipFile(zip_path) as z: + z.extractall(dest_dir) + + +def download_assets(assets: list[tuple[str, Path]]) -> None: + with tempfile.TemporaryDirectory() as tmpdir_str: + tmpdir = Path(tmpdir_str) + for url, dest in assets: + filename = Path(url).name + local_zip = tmpdir / filename + download_to_file(url, local_zip) + extract_zip(local_zip, dest) + print("Asset download and extraction complete.") + + +def main() -> None: + args = parse_args() + assets: dict[int, list[tuple[str, Path]]] = { + 1: [ + ( + "https://lostartefacts.dev/aux/tr1x/main.zip", + Path("data/tr1/ship"), + ) + ], + 2: [ + ( + "https://lostartefacts.dev/aux/tr2x/main.zip", + Path("data/tr2/ship"), + ) + ], + } + match str(args.game_version): + case "1": + download_assets(assets[1]) + case "2": + download_assets(assets[2]) + case "all": + download_assets(assets[1]) + download_assets(assets[2]) + + +if __name__ == "__main__": + main() diff --git a/tools/tr1/inspect_save b/tools/inspect_save similarity index 100% rename from tools/tr1/inspect_save rename to tools/inspect_save diff --git a/tools/shared/docker/config/entrypoint.sh b/tools/shared/docker/config/entrypoint.sh index 3adc4f1a3..ea7fc3de4 100755 --- a/tools/shared/docker/config/entrypoint.sh +++ b/tools/shared/docker/config/entrypoint.sh @@ -12,14 +12,6 @@ export DOTNET_CLI_HOME="/tmp/DOTNET_CLI_HOME" echo $HOME shopt -s globstar -# Build the common lib DLL cd /app/tools/config/ -rm -rf **/bin **/obj -dotnet restore -p:EnableWindowsTargeting=true -dotnet publish -c Release -p:EnableWindowsTargeting=true - -# Build the main executable -cd /app/tools/tr${TR_VERSION}/config/ rm -rf **/bin **/obj **/out/* -dotnet restore -dotnet publish -c Release -o out +dotnet publish TR${TR_VERSION}X_ConfigTool -c Release -o out diff --git a/tools/shared/docker/game_entrypoint.py b/tools/shared/docker/game_entrypoint.py index 949c9ff3f..fcf955ba7 100644 --- a/tools/shared/docker/game_entrypoint.py +++ b/tools/shared/docker/game_entrypoint.py @@ -53,7 +53,7 @@ class PackageOptions(BaseOptions): return [ ( self.build_root / f"TR{self.tr_version}X", - "TR{self.tr_version}X", + f"TR{self.tr_version}X", ) ] @@ -65,7 +65,7 @@ class PackageOptions(BaseOptions): ), ( Path( - f"/app/tools/tr{self.tr_version}/config/out/TR{self.tr_version}X_ConfigTool.exe" + f"/app/tools/config/out/TR{self.tr_version}X_ConfigTool.exe" ), f"TR{self.tr_version}X_ConfigTool.exe", ), diff --git a/tools/shared/docker/installer/entrypoint.sh b/tools/shared/docker/installer/entrypoint.sh index 56ada8697..5c61ae708 100755 --- a/tools/shared/docker/installer/entrypoint.sh +++ b/tools/shared/docker/installer/entrypoint.sh @@ -14,5 +14,4 @@ export DOTNET_CLI_HOME="/tmp/DOTNET_CLI_HOME" shopt -s globstar rm -rf **/bin **/obj **/out/* -dotnet restore dotnet publish TR${TR_VERSION}X_Installer -c Release -o out diff --git a/tools/tr1/config/.gitignore b/tools/tr1/config/.gitignore deleted file mode 100644 index 5fbf210e0..000000000 --- a/tools/tr1/config/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -bin/ -obj/ -out/ -*.pubxml diff --git a/tools/tr1/config/TR1X_ConfigTool.sln b/tools/tr1/config/TR1X_ConfigTool.sln deleted file mode 100644 index e54647185..000000000 --- a/tools/tr1/config/TR1X_ConfigTool.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32407.343 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TR1X_ConfigTool", "TR1X_ConfigTool\TR1X_ConfigTool.csproj", "{DCFCB544-9B88-41D6-A995-EA2D06FE405E}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|x64.ActiveCfg = Debug|x64 - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|x64.Build.0 = Debug|x64 - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|Any CPU.Build.0 = Release|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|x64.ActiveCfg = Release|x64 - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {49C0A7F4-2D17-424B-BD94-A8FC6E95931D} - EndGlobalSection -EndGlobal diff --git a/tools/tr2/config/.gitignore b/tools/tr2/config/.gitignore deleted file mode 100644 index 57193f228..000000000 --- a/tools/tr2/config/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -*.suo -*.o -*.obj -*.pdb -*.lib -*.exp -[Dd]ebug/ -[Rr]elease/ -[Oo]bj/ -*.user -*.ipch -.vs/ -*.vcxproj -*.filters -*.pubxml -out/ diff --git a/tools/tr2/config/TR2X_ConfigTool.sln b/tools/tr2/config/TR2X_ConfigTool.sln deleted file mode 100644 index 8c230ac9e..000000000 --- a/tools/tr2/config/TR2X_ConfigTool.sln +++ /dev/null @@ -1,30 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32407.343 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TR2X_ConfigTool", "TR2X_ConfigTool\TR2X_ConfigTool.csproj", "{DCFCB544-9B88-41D6-A995-EA2D06FE405E}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|x64.ActiveCfg = Debug|x64 - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Debug|x64.Build.0 = Debug|x64 - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|Any CPU.Build.0 = Release|Any CPU - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|x64.ActiveCfg = Release|x64 - {DCFCB544-9B88-41D6-A995-EA2D06FE405E}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {49C0A7F4-2D17-424B-BD94-A8FC6E95931D} - EndGlobalSection -EndGlobal diff --git a/tools/update_gameflow b/tools/update_gameflow index 5d7af2a68..d7192d42a 100755 --- a/tools/update_gameflow +++ b/tools/update_gameflow @@ -68,8 +68,8 @@ def postprocess_game_strings( def process(game: int) -> None: GAME_STRING_DEF_PATHS = [ - PROJECT_PATHS[game].src_dir / "game/game_string.def", SHARED_INCLUDE_DIR / "game/game_string.def", + PROJECT_PATHS[game].src_dir / "game/game_string.def", ] OBJECT_NAMES_DEF_PATH = ( SHARED_INCLUDE_DIR / f"game/objects/names_tr{game}.def"