diff --git a/.github/workflows/openmw.yml b/.github/workflows/openmw.yml index 2b429df0a0..cfc1faa438 100644 --- a/.github/workflows/openmw.yml +++ b/.github/workflows/openmw.yml @@ -1,80 +1,264 @@ name: CMake on: - push: - branches: - - 'master' - pull_request: - branches: [ master ] +- push +- pull_request env: BUILD_TYPE: RelWithDebInfo + VCPKG_DEPS_TAG: 2024-11-10 jobs: Ubuntu: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Add OpenMW PPA Dependencies - run: sudo add-apt-repository ppa:openmw/openmw; sudo apt-get update + - name: Add OpenMW PPA Dependencies + run: sudo add-apt-repository ppa:openmw/openmw; sudo apt-get update - - name: Install Building Dependencies - run: sudo CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic + - name: Install Building Dependencies + run: sudo CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic - - name: Prime ccache - uses: hendrikmuhs/ccache-action@v1 - with: - key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} - max-size: 1000M + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M - - name: Configure - run: cmake . -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=1 -DUSE_SYSTEM_TINYXML=1 -DBUILD_UNITTESTS=1 -DCMAKE_INSTALL_PREFIX=install + - name: Configure + run: > + cmake . + -D CMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + -D OPENMW_USE_SYSTEM_RECASTNAVIGATION=ON + -D USE_SYSTEM_TINYXML=ON + -D BUILD_COMPONENTS_TESTS=ON + -D BUILD_OPENMW_TESTS=ON + -D BUILD_OPENCS_TESTS=ON + -D CMAKE_INSTALL_PREFIX=install - - name: Build - run: make -j3 + - name: Build + run: cmake --build . -- -j$(nproc) - - name: Test - run: ./openmw_test_suite + - name: Run components tests + run: ./components-tests - # - name: Install - # shell: bash - # run: cmake --install . + - name: Run OpenMW tests + run: ./openmw-tests - # - name: Create Artifact - # shell: bash - # working-directory: install - # run: | - # ls -laR - # 7z a ../build_artifact.7z . + - name: Run OpenMW-CS tests + run: ./openmw-cs-tests - # - name: Upload Artifact - # uses: actions/upload-artifact@v1 - # with: - # path: ./build_artifact.7z - # name: build_artifact.7z + # - name: Install + # shell: bash + # run: cmake --install . + + # - name: Create Artifact + # shell: bash + # working-directory: install + # run: | + # ls -laR + # 7z a ../build_artifact.7z . + + # - name: Upload Artifact + # uses: actions/upload-artifact@v1 + # with: + # path: ./build_artifact.7z + # name: build_artifact.7z MacOS: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Install Building Dependencies - run: CI/before_install.osx.sh + - name: Install Building Dependencies + run: CI/before_install.osx.sh - - name: Prime ccache - uses: hendrikmuhs/ccache-action@v1 - with: - key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} - max-size: 1000M + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M - - name: Configure - run: | - rm -fr build # remove the build directory - CI/before_script.osx.sh - - name: Build - run: | - cd build - make -j $(sysctl -n hw.logicalcpu) package + - name: Configure + run: CI/before_script.osx.sh + - name: Build + run: | + cd build + make -j $(sysctl -n hw.logicalcpu) package + + Windows: + strategy: + fail-fast: true + matrix: + image: + - "2019" + - "2022" + + name: windows-${{ matrix.image }} + + runs-on: windows-${{ matrix.image }} + + env: + archive: FAILEDTODOWNLOAD + + steps: + - uses: actions/checkout@v2 + + - name: Create directories for dependencies + run: | + mkdir -p ${{ github.workspace }}/deps + mkdir -p ${{ github.workspace }}/deps/Qt + + - name: Download prebuilt vcpkg packages + working-directory: ${{ github.workspace }}/deps + run: | + $MANIFEST = "vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}.txt" + curl --fail --retry 3 -L -o "$MANIFEST" "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/$MANIFEST" + $lines = Get-Content "$MANIFEST" + $URL = $lines[0] + $split = -split $lines[1] + $HASH = $split[0] + $FILE = $split[1] + curl --fail --retry 3 -L -o "$FILE" "$URL" + $filehash = Get-FileHash "$FILE" -Algorithm SHA512 + if ( $filehash.hash -ne "$HASH" ) { + exit 1 + } + echo "archive=$FILE" >> $env:GITHUB_ENV + + - name: Extract archived prebuilt vcpkg packages + working-directory: ${{ github.workspace }}/deps + run: 7z x -y -ovcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }} ${{ env.archive }} + + - name: Cache Qt + id: qt-cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64 + key: qt-cache-6.6.3-msvc2019_64-v1 + + - name: Download aqt + if: steps.qt-cache.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/deps/Qt + run: > + curl --fail --retry 3 -L + -o aqt_x64.exe + https://github.com/miurahr/aqtinstall/releases/download/v3.1.15/aqt_x64.exe + + - name: Install Qt with aqt + if: steps.qt-cache.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/deps/Qt + run: .\aqt_x64.exe install-qt windows desktop 6.6.3 win64_msvc2019_64 + + - uses: ilammy/msvc-dev-cmd@v1 + + - uses: seanmiddleditch/gha-setup-ninja@master + + - name: Configure OpenMW + run: > + cmake + -S . + -B ${{ github.workspace }}/build + -G Ninja + -D CMAKE_BUILD_TYPE=RelWithDebInfo + -D CMAKE_TOOLCHAIN_FILE='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/scripts/buildsystems/vcpkg.cmake' + -D CMAKE_PREFIX_PATH='${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64' + -D LuaJit_INCLUDE_DIR='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/include/luajit' + -D LuaJit_LIBRARY='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/lib/lua51.lib' + -D BUILD_BENCHMARKS=ON + -D BUILD_COMPONENTS_TESTS=ON + -D BUILD_OPENMW_TESTS=ON + -D BUILD_OPENCS_TESTS=ON + -D OPENMW_USE_SYSTEM_SQLITE3=OFF + -D OPENMW_USE_SYSTEM_YAML_CPP=OFF + -D OPENMW_LTO_BUILD=ON + + - name: Build OpenMW + run: cmake --build ${{ github.workspace }}/build + + - name: Install OpenMW + run: cmake --install ${{ github.workspace }}/build --prefix ${{ github.workspace }}/install + + - name: Copy missing DLLs + run: | + cp ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/bin/Release/MyGUIEngine.dll ${{ github.workspace }}/install + cp -Filter *.dll -Recurse ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/bin/osgPlugins-3.6.5 ${{ github.workspace }}/install + cp ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/bin/*.dll ${{ github.workspace }}/install + + - name: Copy Qt DLLs + working-directory: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64 + run: | + cp bin/Qt6Core.dll ${{ github.workspace }}/install + cp bin/Qt6Gui.dll ${{ github.workspace }}/install + cp bin/Qt6Network.dll ${{ github.workspace }}/install + cp bin/Qt6OpenGL.dll ${{ github.workspace }}/install + cp bin/Qt6OpenGLWidgets.dll ${{ github.workspace }}/install + cp bin/Qt6Widgets.dll ${{ github.workspace }}/install + cp bin/Qt6Svg.dll ${{ github.workspace }}/install + mkdir ${{ github.workspace }}/install/styles + cp plugins/styles/qwindowsvistastyle.dll ${{ github.workspace }}/install/styles + mkdir ${{ github.workspace }}/install/platforms + cp plugins/platforms/qwindows.dll ${{ github.workspace }}/install/platforms + mkdir ${{ github.workspace }}/install/imageformats + cp plugins/imageformats/qsvg.dll ${{ github.workspace }}/install/imageformats + mkdir ${{ github.workspace }}/install/iconengines + cp plugins/iconengines/qsvgicon.dll ${{ github.workspace }}/install/iconengines + + - name: Move pdb files + run: | + robocopy install pdb *.pdb /MOVE + if ($lastexitcode -lt 8) { + $global:LASTEXITCODE = $null + } + + - name: Remove extra pdb files + shell: bash + run: | + rm -rf install/bin + rm -rf install/_deps + + - name: Generate CI-ID.txt + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + job_url=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json jobs --jq '.jobs[] | select(.name == "windows-${{ matrix.image }}") | .url') + printf "Ref ${{ github.ref }}\nJob ${job_url}\nCommit ${{ github.sha }}\n" > install/CI-ID.txt + cp install/CI-ID.txt pdb/CI-ID.txt + + - name: Store OpenMW archived pdb files + uses: actions/upload-artifact@v4 + with: + name: openmw-windows-${{ matrix.image }}-pdb-${{ github.sha }} + path: ${{ github.workspace }}/pdb/* + + - name: Store OpenMW build artifacts + uses: actions/upload-artifact@v4 + with: + name: openmw-windows-${{ matrix.image }}-${{ github.sha }} + path: ${{ github.workspace }}/install/* + + - name: Add install directory to PATH + shell: bash + run: echo '${{ github.workspace }}/install' >> ${GITHUB_PATH} + + - name: Run components tests + run: build/components-tests.exe + + - name: Run OpenMW tests + run: build/openmw-tests.exe + + - name: Run OpenMW-CS tests + run: build/openmw-cs-tests.exe + + - name: Run detournavigator navmeshtilescache benchmark + run: build/openmw_detournavigator_navmeshtilescache_benchmark.exe + + - name: Run settings access benchmark + run: build/openmw_settings_access_benchmark.exe + + - name: Run esm refid benchmark + run: build/openmw_esm_refid_benchmark.exe diff --git a/.gitignore b/.gitignore index f25adf58e6..39033bd725 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ Doxygen .idea cmake-build-* files/windows/*.aps +.cache/clangd ## qt-creator CMakeLists.txt.user* .vs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6201785e8c..f1da3eb43c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,11 +22,11 @@ variables: # These can be specified per job or per pipeline ARTIFACT_COMPRESSION_LEVEL: "fast" CACHE_COMPRESSION_LEVEL: "fast" + FF_TIMESTAMPS: "true" .Ubuntu_Image: tags: - - docker - - linux + - saas-linux-medium-amd64 image: ubuntu:22.04 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" @@ -70,15 +70,16 @@ Ubuntu_GCC_preprocess: - df -h - du -sh . - cmake --install . - - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite --gtest_output="xml:openmw_tests.xml"; fi - - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-cs-tests --gtest_output="xml:openmw_cs_tests.xml"; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./components-tests --gtest_output="xml:components-tests.xml"; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-tests --gtest_output="xml:openmw-tests.xml"; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-cs-tests --gtest_output="xml:openmw-cs-tests.xml"; fi - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_detournavigator_navmeshtilescache_benchmark; fi - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_esm_refid_benchmark; fi - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi - ccache -s - df -h - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi - - ls | grep -v -e '^extern$' -e '^install$' -e '^openmw_tests.xml$' -e '^openmw_cs_tests.xml$' | xargs -I '{}' rm -rf './{}' + - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}' - cd .. - df -h - du -sh build/ @@ -91,8 +92,7 @@ Ubuntu_GCC_preprocess: Coverity: tags: - - docker - - linux + - saas-linux-medium-amd64 image: ubuntu:22.04 stage: build rules: @@ -116,6 +116,7 @@ Coverity: script: - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" + - export COVERITY_NO_LOG_ENVIRONMENT_VARIABLES=1 - mkdir -pv "${CCACHE_DIR}" - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.linux.sh @@ -142,7 +143,7 @@ Ubuntu_GCC: variables: CC: gcc CXX: g++ - CCACHE_SIZE: 4G + CCACHE_SIZE: 3G # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h @@ -172,6 +173,20 @@ Clang_Format: - CI/check_file_names.sh - CI/check_clang_format.sh +Lupdate: + extends: .Ubuntu_Image + stage: checks + cache: + key: Ubuntu_lupdate.ubuntu_22.04.v1 + paths: + - apt-cache/ + variables: + LUPDATE: lupdate + before_script: + - CI/install_debian_deps.sh openmw-qt-translations + script: + - CI/check_qt_translations.sh + Teal: stage: checks extends: .Ubuntu_Image @@ -181,8 +196,9 @@ Teal: script: - CI/teal_ci.sh artifacts: + when: always paths: - - teal_declarations.zip + - teal_declarations Ubuntu_GCC_Debug: extends: .Ubuntu @@ -193,9 +209,10 @@ Ubuntu_GCC_Debug: variables: CC: gcc CXX: g++ - CCACHE_SIZE: 4G + CCACHE_SIZE: 3G CMAKE_BUILD_TYPE: Debug CMAKE_CXX_FLAGS_DEBUG: -O0 + BUILD_SHARED_LIBS: 1 # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h @@ -408,7 +425,7 @@ Ubuntu_Clang_Tidy_other: needs: - Ubuntu_Clang_Tidy_components variables: - BUILD_TARGETS: bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest openmw_test_suite openmw-navmeshtool openmw-bulletobjecttool + BUILD_TARGETS: bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest components-tests openmw-tests openmw-cs-tests openmw-navmeshtool openmw-bulletobjecttool timeout: 3h .Ubuntu_Clang_tests: @@ -445,6 +462,7 @@ Ubuntu_Clang_tests_Debug: stage: test variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + EXAMPLE_SUITE_REVISION: f51b832e033429a7cdc520e0e48d7dfdb9141caa cache: paths: - .cache/pip @@ -484,7 +502,6 @@ Ubuntu_GCC_integration_tests_asan: paths: - ccache/ script: - - rm -fr build # remove the build directory - CI/before_install.osx.sh - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" @@ -492,7 +509,7 @@ Ubuntu_GCC_integration_tests_asan: - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.osx.sh - cd build; make -j $(sysctl -n hw.logicalcpu) package - - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}_${CI_JOB_ID}.dmg"; done + - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}.dmg"; done - | if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then artifactDirectory="${CI_PROJECT_NAMESPACE//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_REF_NAME//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_SHORT_SHA//[\"<>|$'\t'\/\\?*]/_}-${CI_JOB_ID//[\"<>|$'\t'\/\\?*]/_}/" @@ -504,21 +521,59 @@ Ubuntu_GCC_integration_tests_asan: artifacts: paths: - build/OpenMW-*.dmg - - "build/**/*.log" -macOS13_Xcode14_arm64: +macOS14_Xcode15_arm64: extends: .MacOS - image: macos-12-xcode-14 + image: macos-14-xcode-15 tags: - saas-macos-medium-m1 cache: - key: macOS12_Xcode14_arm64.v4 + key: macOS14_Xcode15_arm64.v1 variables: CCACHE_SIZE: 3G +.Compress_And_Upload_Symbols_Base: + extends: .Ubuntu_Image + stage: build + variables: + GIT_STRATEGY: none + script: + - apt-get update + - apt-get install -y curl gcab unzip + - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscli-exe-linux-x86_64.zip + - unzip -d awscli-exe-linux-x86_64 awscli-exe-linux-x86_64.zip + - pushd awscli-exe-linux-x86_64 + - ./aws/install + - popd + - aws --version + - unzip -d sym_store *sym_store.zip + - shopt -s globstar + - | + for file in sym_store/**/*.exe; do + if [[ -f "$file" ]]; then + gcab --create --zip --nopath "${file%.exe}.ex_" "$file" + fi + done + - | + for file in sym_store/**/*.dll; do + if [[ -f "$file" ]]; then + gcab --create --zip --nopath "${file%.dll}.dl_" "$file" + fi + done + - | + for file in sym_store/**/*.pdb; do + if [[ -f "$file" ]]; then + gcab --create --zip --nopath "${file%.pdb}.pd_" "$file" + fi + done + - | + if [[ -v AWS_ACCESS_KEY_ID ]]; then + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude '*' --include '*.ex_' --include '*.dl_' --include '*.pd_' sym_store s3://openmw-sym + fi + .Windows_Ninja_Base: tags: - - windows + - saas-windows-medium-amd64 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" before_script: @@ -555,60 +610,53 @@ macOS13_Xcode14_arm64: - $env:CCACHE_BASEDIR = Get-Location - $env:CCACHE_DIR = "$(Get-Location)\ccache" - New-Item -Type Directory -Force -Path $env:CCACHE_DIR - - New-Item -Type File -Force -Path MSVC2019_64_Ninja\.cmake\api\v1\query\codemodel-v2 - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N -b -t -C $multiview -E + - New-Item -Type File -Force -Path MSVC2022_64_Ninja\.cmake\api\v1\query\codemodel-v2 + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2022 -k -V -N -b -t -C $multiview -E - Get-Volume - - cd MSVC2019_64_Ninja + - cd MSVC2022_64_Ninja - .\ActivateMSVC.ps1 - - cmake --build . --config $config - - ccache --show-stats + - cmake --build . --config $config --target $targets + - ccache --show-stats -v - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt - $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/" - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} } Push-Location .. - ..\CI\Store-Symbols.ps1 - if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude * --include *.ex_ --include *.dl_ --include *.pd_ .\SymStore s3://openmw-sym - } - 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + ..\CI\Store-Symbols.ps1 -SkipCompress + 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' - | if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} } - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-v7 + key: ninja-2022-v11 paths: - ccache - deps - - MSVC2019_64_Ninja/deps/Qt + - MSVC2022_64_Ninja/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - - MSVC2019_64_Ninja/*.log - - MSVC2019_64_Ninja/*/*.log - - MSVC2019_64_Ninja/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log + - MSVC2022_64_Ninja/*.log + - MSVC2022_64_Ninja/**/*.log + variables: + targets: all # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h @@ -618,6 +666,13 @@ macOS13_Xcode14_arm64: variables: config: "Release" +.Windows_Compress_And_Upload_Symbols_Ninja_Release: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_Release" + artifacts: true + .Windows_Ninja_Release_MultiView: extends: - .Windows_Ninja_Base @@ -625,23 +680,53 @@ macOS13_Xcode14_arm64: multiview: "-M" config: "Release" +.Windows_Compress_And_Upload_Symbols_Ninja_Release_MultiView: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_Release_MultiView" + artifacts: true + .Windows_Ninja_Debug: extends: - .Windows_Ninja_Base variables: config: "Debug" +.Windows_Compress_And_Upload_Symbols_Ninja_Debug: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_Debug" + artifacts: true + .Windows_Ninja_RelWithDebInfo: extends: - .Windows_Ninja_Base variables: config: "RelWithDebInfo" # Gitlab can't successfully execute following binaries due to unknown reason - # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + # executables: "components-tests.exe,openmw-tests.exe,openmw-cs-tests.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + +.Windows_Compress_And_Upload_Symbols_Ninja_RelWithDebInfo: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_RelWithDebInfo" + artifacts: true + +.Windows_Ninja_CacheInit: + # currently, Windows jobs for all configs share the same cache key as we only cache the dependencies + extends: + - .Windows_Ninja_Base + variables: + config: "RelWithDebInfo" + targets: "get-version" + when: manual .Windows_MSBuild_Base: tags: - - windows + - saas-windows-medium-amd64 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" before_script: @@ -651,7 +736,6 @@ macOS13_Xcode14_arm64: - choco source disable -n=chocolatey - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y - - choco install ccache -y - choco install vswhere -y - choco install python -y - choco install awscli -y @@ -674,62 +758,50 @@ macOS13_Xcode14_arm64: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - - $env:CCACHE_BASEDIR = Get-Location - - $env:CCACHE_DIR = "$(Get-Location)\ccache" - - New-Item -Type Directory -Force -Path $env:CCACHE_DIR - - New-Item -Type File -Force -Path MSVC2019_64\.cmake\api\v1\query\codemodel-v2 - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -b -t -C $multiview -E - - cd MSVC2019_64 + - New-Item -Type File -Force -Path MSVC2022_64\.cmake\api\v1\query\codemodel-v2 + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2022 -k -V -b -t -C $multiview -E + - cd MSVC2022_64 - Get-Volume - - cmake --build . --config $config - - ccache --show-stats + - cmake --build . --config $config --target $targets - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt - $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/" - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} } Push-Location .. - ..\CI\Store-Symbols.ps1 - if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude * --include *.ex_ --include *.dl_ --include *.pd_ .\SymStore s3://openmw-sym - } - 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + ..\CI\Store-Symbols.ps1 -SkipCompress + 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' - | if (Test-Path env:AWS_ACCESS_KEY_ID) { - aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} } - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-v7 + key: msbuild-2022-v11 paths: - - ccache - deps - - MSVC2019_64/deps/Qt + - MSVC2022_64/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - - MSVC2019_64/*.log - - MSVC2019_64/*/*.log - - MSVC2019_64/*/*/*.log - - MSVC2019_64/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*/*/*.log + - MSVC2022_64/*.log + - MSVC2022_64/**/*.log + variables: + targets: ALL_BUILD # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h @@ -739,27 +811,61 @@ macOS13_Xcode14_arm64: variables: config: "Release" +.Windows_Compress_And_Upload_Symbols_MSBuild_Release: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_MSBuild_Release" + artifacts: true + .Windows_MSBuild_Debug: extends: - .Windows_MSBuild_Base variables: config: "Debug" +.Windows_Compress_And_Upload_Symbols_MSBuild_Debug: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_MSBuild_Debug" + artifacts: true + Windows_MSBuild_RelWithDebInfo: extends: - .Windows_MSBuild_Base variables: config: "RelWithDebInfo" # Gitlab can't successfully execute following binaries due to unknown reason - # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + # executables: "components-tests.exe,openmw-tests.exe,openmw-cs-tests.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" # temporarily enabled while we're linking these on the downloads page rules: # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule" +Windows_Compress_And_Upload_Symbols_MSBuild_RelWithDebInfo: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_MSBuild_RelWithDebInfo" + artifacts: true + # temporarily enabled while we're linking the above on the downloads page + rules: + # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule" + +Windows_MSBuild_CacheInit: + # currently, Windows jobs for all configs share the same cache key as we only cache the dependencies + extends: + - .Windows_MSBuild_Base + variables: + config: "RelWithDebInfo" + targets: "get-version" + when: manual + .Ubuntu_AndroidNDK_arm64-v8a: tags: - - linux + - saas-linux-medium-amd64 image: psi29a/android-ndk:focal-ndk22 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e0b39ec495..962b34f516 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,10 @@ sphinx: configuration: docs/source/conf.py python: - version: 3.8 install: - requirements: docs/requirements.txt +build: + os: ubuntu-22.04 + tools: + python: "3.8" diff --git a/AUTHORS.md b/AUTHORS.md index 99080fdebd..e5caf5fa58 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -15,6 +15,7 @@ Programmers Nicolay Korslund - Project leader 2008-2010 scrawl - Top contributor + AbduSharif Adam Hogan (aurix) Aesylwinn aegis @@ -60,6 +61,7 @@ Programmers Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) crussell187 + Sam Hellawell (cykoder) Dan Vukelich (sanchezman) darkf Dave Corley (S3ctor) @@ -79,6 +81,7 @@ Programmers Eduard Cot (trombonecot) Eli2 Emanuel Guével (potatoesmaster) + Epoch Eris Caffee (eris) eroen escondida @@ -96,6 +99,7 @@ Programmers gugus/gus guidoj Haoda Wang (h313) + holorat hristoast Internecine Ivan Beloborodov (myrix) @@ -140,6 +144,8 @@ Programmers Lordrea Łukasz Gołębiewski (lukago) Lukasz Gromanowski (lgro) + Mads Sandvei (Foal) + Maksim Eremenko (Max Yari) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) @@ -150,6 +156,7 @@ Programmers Mateusz Malisz (malice) Max Henzerling (SaintMercury) megaton + Mehdi Yousfi-Monod (mym) Michael Hogan (Xethik) Michael Mc Donnell Michael Papageorgiou (werdanith) @@ -186,6 +193,7 @@ Programmers pkubik PLkolek PlutonicOverkill + Qlonever Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) Randy Davin (Kindi) @@ -225,6 +233,7 @@ Programmers thegriglat Thomas Luppi (Digmaster) tlmullis + trav tri4ng1e Thoronador Tobias Tribble (zackhasacat) @@ -244,6 +253,7 @@ Programmers xyzz Yohaulticetl Yuri Krupenin + Yury Stepovikov zelurker Documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee31b6762..452e64f7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,80 +2,247 @@ ------ Bug #2623: Snowy Granius doesn't prioritize conjuration spells + Bug #3438: NPCs can't hit bull netch with melee weapons Bug #3842: Body part skeletons override the main skeleton Bug #4127: Weapon animation looks choppy Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game Bug #4207: RestoreHealth/Fatigue spells have a huge priority even if a success chance is near 0 Bug #4382: Sound output device does not change when it should + Bug #4508: Can't stack enchantment buffs from different instances of the same self-cast generic magic apparel Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely + Bug #4683: Disposition decrease when player commits crime is not implemented properly + Bug #4742: Actors with wander never stop walking after Loopgroup Walkforward + Bug #4743: PlayGroup doesn't play non-looping animations correctly Bug #4754: Stack of ammunition cannot be equipped partially Bug #4816: GetWeaponDrawn returns 1 before weapon is attached + Bug #4822: Non-weapon equipment and body parts can't inherit time from parent animation + Bug #4898: Odd/Incorrect lighting on meshes Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses + Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation + Bug #5065: Actors with scripted animation still try to wander and turn around without moving + Bug #5066: Quirks with starting and stopping scripted animations Bug #5129: Stuttering animation on Centurion Archer + Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place Bug #5371: Keyframe animation tracks are used for any file that begins with an X + Bug #5413: Enemies do a battlecry everytime the player summons a creature Bug #5714: Touch spells cast using ExplodeSpell don't always explode + Bug #5755: Reset friendly hit counter Bug #5849: Paralysis breaks landing Bug #5870: Disposing of actors who were selected in the console doesn't deselect them like vanilla Bug #5883: Immobile creatures don't cause water ripples Bug #5977: Fatigueless NPCs' corpse underwater changes animation on game load Bug #6025: Subrecords cannot overlap records Bug #6027: Collisionshape becomes spiderweb-like when the mesh is too complex + Bug #6097: Level Progress Tooltip Sometimes Not Updated + Bug #6146: Lua command `actor:setEquipment` doesn't trigger mwscripts when equipping or unequipping a scripted item + Bug #6156: 1ft Charm or Sound magic effect vfx doesn't work properly + Bug #6190: Unintuitive sun specularity time of day dependence Bug #6222: global map cell size can crash openmw if set to too high a value + Bug #6240: State sharing sometimes prevents the use of the same texture file for different purposes in shaders Bug #6313: Followers with high Fight can turn hostile + Bug #6402: The sound of a thunderstorm does not stop playing after entering the premises Bug #6427: Enemy health bar disappears before damaging effect ends Bug #6550: Cloned body parts don't inherit texture effects + Bug #6574: Crash at far away from world origin coordinates Bug #6645: Enemy block sounds align with animation instead of blocked hits Bug #6657: Distant terrain tiles become black when using FWIW mod Bug #6661: Saved games that have no preview screenshot cause issues or crashes + Bug #6665: The kobolds in the skyrim: home of the nords mod are oversized Bug #6716: mwscript comparison operator handling is too restrictive + Bug #6723: "Turn to movement direction" makes the player rotate wildly with COLLADA + Bug #6754: Beast to Non-beast transformation mod is not working on OpenMW + Bug #6758: Main menu background video can be stopped by opening the options menu Bug #6807: Ultimate Galleon is not working properly + Bug #6846: Launcher only works with default config paths Bug #6893: Lua: Inconsistent behavior with actors affected by Disable and SetDelete commands Bug #6894: Added item combines with equipped stack instead of creating a new unequipped stack + Bug #6932: Creatures flee from my followers and we have to chase after them Bug #6939: OpenMW-CS: ID columns are too short Bug #6949: Sun Damage effect doesn't work in quasi exteriors Bug #6964: Nerasa Dralor Won't Follow Bug #6973: Fade in happens after the scene load and is shown Bug #6974: Only harmful effects are reflected Bug #6977: Sun damage implementation does not match research + Bug #6985: Issues with Magic Cards numbers readability Bug #6986: Sound magic effect does not make noise Bug #6987: Set/Mod Blindness should not darken the screen Bug #6992: Crossbow reloading doesn't look the same as in Morrowind Bug #6993: Shooting your last round of ammunition causes the attack animation to cancel Bug #7009: Falling actors teleport to the ground without receiving any damage on cell loading + Bug #7013: Local map rendering in some cells is broken Bug #7034: Misc items defined in one content file are not treated as keys if another content file uses them as such + Bug #7040: Incorrect rendering order for Rebirth's Stormfang Bug #7042: Weapon follow animations that immediately follow the hit animations cause multiple hits Bug #7044: Changing a class' services does not affect autocalculated NPCs + Bug #7051: Collada animated character models are optimized out of the collision box instance with object paging + Bug #7053: Running into objects doesn't trigger GetCollidingPC Bug #7054: Quests aren't sorted by name Bug #7064: NPCs don't report crime if the player is casting offensive spells on them while sneaking Bug #7077: OpenMW fails to load certain particle effects in .osgt format Bug #7084: Resurrecting an actor doesn't take into account base record changes Bug #7088: Deleting last save game of last character doesn't clear character name/details Bug #7092: BSA archives from higher priority directories don't take priority + Bug #7102: Some HQ Creatures mod models can hit the 8 texture slots limit with 0.48 + Bug #7103: Multiple paths pointing to the same plugin but with different cases lead to automatically removed config entries Bug #7122: Teleportation to underwater should cancel active water walking effect Bug #7131: MyGUI log spam when post processing HUD is open Bug #7134: Saves with an invalid last generated RefNum can be loaded + Bug #7145: Normals passed to post-processing shaders are broken + Bug #7146: Debug draw for normals is wrong Bug #7163: Myar Aranath: Wheat breaks the GUI + Bug #7168: Fix average scene luminance Bug #7172: Current music playlist continues playing indefinitely if next playlist is empty + Bug #7202: Post-processing normals for terrain, water randomly stop rendering + Bug #7204: Missing actor scripts freeze the game Bug #7229: Error marker loading failure is not handled Bug #7243: Supporting loading external files from VFS from esm files Bug #7284: "Your weapon has no effect." message doesn't always show when the player character attempts to attack + Bug #7292: Weather settings for disabling or enabling snow and rain ripples don't work Bug #7298: Water ripples from projectiles sometimes are not spawned Bug #7307: Alchemy "Magic Effect" search string does not match on tool tip for effects related to attributes + Bug #7309: Sunlight scattering is visible in inappropriate situations Bug #7322: Shadows don't cover groundcover depending on the view angle and perspective with compute scene bounds = primitives + Bug #7351: Unsupported MSAA level fallback wrecks GL context extension checks + Bug #7353: Normal Map Crashes with Starwind Assets in TES3MP and OpenMW + Bug #7354: Disabling post processing in-game causes a crash + Bug #7364: Post processing is not reflected in savegame previews + Bug #7380: NiZBufferProperty issue Bug #7413: Generated wilderness cells don't spawn fish Bug #7415: Unbreakable lock discrepancies + Bug #7416: Modpccrimelevel is different from vanilla Bug #7428: AutoCalc flag is not used to calculate enchantment costs + Bug #7447: OpenMW-CS: Dragging a cell of a different type (from the initial type) into the 3D view crashes OpenMW-CS Bug #7450: Evading obstacles does not work for actors missing certain animations Bug #7459: Icons get stacked on the cursor when picking up multiple items simultaneously + Bug #7469: Reloading lua with a orphaned lua UI element causes crash Bug #7472: Crash when enchanting last projectiles + Bug #7475: Equipping a constant effect item doesn't update the magic menu + Bug #7502: Data directories dialog (0.48.0) forces adding subdirectory instead of intended directory Bug #7505: Distant terrain does not support sample size greater than cell size + Bug #7535: Bookart paths for textures in OpenMW vs vanilla Morrowind + Bug #7548: Actors cannot open doors that were teleported from a different cell Bug #7553: Faction reaction loading is incorrect + Bug #7557: Terrain::ChunkManager::createChunk is called twice for the same position, lod on initial loading + Bug #7573: Drain Fatigue can't bring fatigue below zero by default + Bug #7582: Skill specializations are hardcoded in character creation + Bug #7585: Difference in interior lighting between OpenMW with legacy lighting method enabled and vanilla Morrowind + Bug #7587: Quick load related crash + Bug #7603: Scripts menu size is not updated properly + Bug #7604: Goblins Grunt becomes idle once injured + Bug #7609: ForceGreeting should not open dialogue for werewolves + Bug #7611: Beast races' idle animations slide after turning or jumping in place + Bug #7617: The death prompt asks the player if they wanted to load the character's last created save + Bug #7619: Long map notes may get cut off + Bug #7623: Incorrect placement of the script info in the engraved ring of healing tooltip + Bug #7627: Сrash at the start + Bug #7630: Charm can be cast on creatures + Bug #7631: Cannot trade with/talk to Creeper or Mudcrab Merchant when they're fleeing + Bug #7633: Groundcover should ignore non-geometry Drawables + Bug #7636: Animations bug out when switching between 1st and 3rd person, while playing a scripted animation + Bug #7637: Actors can sometimes move while playing scripted animations + Bug #7639: NPCs don't use hand-to-hand if their other melee skills were damaged during combat + Bug #7641: loopgroup loops the animation one time too many for actors + Bug #7642: Items in repair and recharge menus aren't sorted alphabetically + Bug #7643: Can't enchant items with constant effect on self magic effects for non-player character + Bug #7646: Follower voices pain sounds when attacked with magic + Bug #7647: NPC walk cycle bugs after greeting player + Bug #7654: Tooltips for enchantments with invalid effects cause crashes + Bug #7660: Some inconsistencies regarding Invisibility breaking + Bug #7661: Player followers should stop attacking newly recruited actors + Bug #7665: Alchemy menu is missing the ability to deselect and choose different qualities of an apparatus + Bug #7675: Successful lock spell doesn't produce a sound + Bug #7676: Incorrect magic effect order in alchemy + Bug #7679: Scene luminance value flashes when toggling shaders + Bug #7685: Corky sometimes doesn't follow Llovyn Andus + Bug #7696: Freeze in CompositeMapRenderer::drawImplementation + Bug #7707: (OpenCS): New landscape records do not contain appropriate flags + Bug #7712: Casting doesn't support spells and enchantments with no effects + Bug #7721: CS: Special Chars Not Allowed in IDs + Bug #7723: Assaulting vampires and werewolves shouldn't be a crime + Bug #7724: Guards don't help vs werewolves + Bug #7728: Fatal Error at Startup + Bug #7733: Launcher shows incorrect data paths when there's two plugins with the same name + Bug #7737: OSG stats are missing some data on loading screens + Bug #7742: Governing attribute training limit should use the modified attribute + Bug #7744: Player base record cannot have weapons in the inventory + Bug #7753: Editor: Actors Don't Scale According to Their Race + Bug #7758: Water walking is not taken into account to compute path cost on the water + Bug #7761: Rain and ambient loop sounds are mutually exclusive + Bug #7763: Bullet shape loading problems, assorted + Bug #7765: OpenMW-CS: Touch Record option is broken + Bug #7769: Sword of the Perithia: Broken NPCs + Bug #7770: Sword of the Perithia: Script execution failure + Bug #7780: Non-ASCII texture paths in NIF files don't work + Bug #7785: OpenMW-CS initialising Skill and Attribute fields to 0 instead of -1 on non-FortifyStat spells + Bug #7787: Crashing when loading a saved game (not always though) + Bug #7794: Fleeing NPCs name tooltip doesn't appear + Bug #7796: Absorbed enchantments don't restore magicka + Bug #7823: Game crashes when launching it. + Bug #7832: Ingredient tooltips show magnitude for Fortify Maximum Magicka effect + Bug #7840: First run of the launcher doesn't save viewing distance as the default value + Bug #7841: Editor: "Dirty" water heights are saved in modified CELLs + Bug #7859: AutoCalc flag is not used to calculate potion value + Bug #7861: OpenMW-CS: Incorrect DIAL's type in INFO records + Bug #7872: Region sounds use wrong odds + Bug #7886: Equip and unequip animations can't share the animation track section + Bug #7887: Editor: Mismatched reported script data size and actual data size causes a crash during save + Bug #7891: Launcher Reverts 8k Shadows to default + Bug #7896: Editor: Loading cellrefs incorrectly transforms Refnums, causing load failures + Bug #7898: Editor: Invalid reference scales are allowed + Bug #7899: Editor: Doors can't be unlocked + Bug #7901: Editor: Teleport-related fields shouldn't be editable if a ref does not teleport + Bug #7908: Key bindings names in the settings menu are layout-specific + Bug #7912: Lua: castRenderingRay fails to hit height map + Bug #7943: Using "addSoulGem" and "dropSoulGem" commands to creatures works only with "Weapon & Shield" flagged ones + Bug #7950: Crash in MWPhysics::PhysicsTaskScheduler::removeCollisionObject + Bug #7970: Difference of GetPCSleep (?) behavior between vanilla and OpenMW + Bug #7980: Paralyzed NPCs' lips move + Bug #7993: Cannot load Bloodmoon without Tribunal + Bug #7997: Can toggle perspective when paralyzed + Bug #8002: Portable light sources held by creatures do not emit lighting + Bug #8005: F3 stats bars are sorted not according to their place in the timeline + Bug #8018: Potion effects should never explode and always apply on self + Bug #8021: Player's scale doesn't reset when starting a new game + Bug #8048: Actors can generate negative collision extents and have no collision + Bug #8063: menu_background.bik video with audio freezes the game forever + Bug #8064: Lua move360 script doesn't respect the enableZoom/disableZoom Camera interface setting + Bug #8085: Don't search in scripts or shaders directories for "Select directories you wish to add" menu in launcher + Bug #8097: GetEffect doesn't detect 0 magnitude spells + Bug #8099: Reaching Lua memory limit leads to a crash + Bug #8124: Normal weapon resistance is applied twice for NPCs + Bug #8132: Actors without hello responses turn to face the player + Bug #8171: Items with more than 100% health can be repaired + Bug #8172: Openmw-cs crashes when viewing `Dantooine, Sea` + Bug #8187: Intervention effects should use Chebyshev distance to determine the closest marker + Bug #8189: The import tab in the launcher doesn't remember the checkbox selection + Bug #8191: NiRollController does not work for sheath meshes + Bug #8206: Moving away from storm wind origin should make you faster + Bug #8207: Using hand-to-hand while sneaking plays the critical hit sound when the target is not getting hurt + Bug #8208: The launcher's view distance option's minimum value isn't capped to Vanilla's minimum + Bug #8223: Ghosts don't move while spellcasting + Bug #8231: AGOP doesn't like NiCollisionSwitch + Bug #8237: Non-bipedal creatures should *not* use spellcast equip/unequip animations + Feature #1415: Infinite fall failsafe + Feature #2566: Handle NAM9 records for manual cell references + Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking Feature #3537: Shader-based water ripples + Feature #5173: Support for NiFogProperty Feature #5492: Let rain and snow collide with statics + Feature #5926: Refraction based on water depth + Feature #5944: Option to use camera as sound listener + Feature #6009: Animation blending - smooth animation transitions with modding support + Feature #6152: Playing music via lua scripts + Feature #6188: Specular lighting from point light sources + Feature #6411: Support translations in openmw-launcher Feature #6447: Add LOD support to Object Paging Feature #6491: Add support for Qt6 + Feature #6505: UTF-8 support in Lua scripts Feature #6556: Lua API for sounds + Feature #6679: Design a custom Input Action API Feature #6726: Lua API for creating new objects + Feature #6727: Lua API for records of all object types + Feature #6823: Animation layering for osgAnimation formats + Feature #6864: Lua file access API Feature #6922: Improve launcher appearance Feature #6933: Support high-resolution cursor textures Feature #6945: Support S3TC-compressed and BGR/BGRA NiPixelData @@ -84,21 +251,75 @@ Feature #6995: Localize the "show effect duration" option Feature #7058: Implement TestModels (T3D) console command Feature #7087: Block resolution change in the Windowed Fullscreen mode + Feature #7091: Allow passing `initData` to the :addScript call Feature #7125: Remembering console commands between sessions Feature #7129: Add support for non-adaptive VSync Feature #7130: Ability to set MyGUI logging verbosity + Feature #7142: MWScript Lua API Feature #7148: Optimize string literal lookup in mwscript + Feature #7160: Editor: Moving the Response column of Topicinfos in a better place + Feature #7161: OpenMW-CS: Make adding and filtering TopicInfos easier + Feature #7180: Rename water_nm file and move it to the vfs Feature #7194: Ori to show texture paths Feature #7214: Searching in the in-game console - Feature #7284: Searching in the console with regex and toggleable case-sensitivity + Feature #7245: Expose the argument `cancelOther` of `AiSequence::stack` to Lua + Feature #7248: Searching in the console with regex and toggleable case-sensitivity + Feature #7318: Ability to disable water culling + Feature #7468: Factions API for Lua Feature #7477: NegativeLight Magic Effect flag Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field + Feature #7538: Lua API for advancing skills Feature #7546: Start the game on Fredas + Feature #7554: Controller binding for tab for menu navigation + Feature #7568: Uninterruptable scripted music + Feature #7590: [Lua] Ability to deserialize YAML data from scripts + Feature #7606: Launcher: allow Shift-select in Archives tab + Feature #7608: Make the missing dependencies warning when loading a savegame more helpful + Feature #7618: Show the player character's health in the save details + Feature #7625: Add some missing console error outputs + Feature #7634: Support NiParticleBomb + Feature #7648: Lua Save game API + Feature #7652: Sort inactive post processing shaders list properly + Feature #7698: Implement sAbsorb, sDamage, sDrain, sFortify and sRestore + Feature #7709: Improve resolution selection in Launcher + Feature #7777: Support external Bethesda material files (BGSM/BGEM) + Feature #7788: [Lua] Add ignore option to nearby.castRenderingRay + Feature #7792: Support Timescale Clouds + Feature #7795: Support MaxNumberRipples INI setting + Feature #7805: Lua Menu context + Feature #7860: Lua: Expose NPC AI settings (fight, alarm, flee) + Feature #7875: Disable MyGUI windows snapping + Feature #7914: Do not allow to move GUI windows out of screen + Feature #7916: Expose all AiWander options to Lua, extend other packages as well + Feature #7923: Don't show non-existent higher ranks for factions with fewer than 9 ranks + Feature #7932: Support two-channel normal maps + Feature #7936: Scalable icons in Qt applications + Feature #7953: Allow to change SVG icons colors depending on color scheme + Feature #7964: Add Lua read access to MW Dialogue records + Feature #7971: Make save's Time Played value display hours instead of days + Feature #7985: Support dark mode on Windows + Feature #8038: (Lua) Containers should have respawning/organic flags + Feature #8067: Support Game Mode on macOS + Feature #8078: OpenMW-CS Terrain Equalize Tool + Feature #8087: Creature movement flags are not exposed + Feature #8092: Lua - Vector swizzling + Feature #8109: Expose commitCrime to Lua API + Feature #8130: Launcher: Add the ability to open a selected data directory in the file browser + Feature #8145: Starter spell flag is not exposed + Task #5859: User openmw-cs.cfg has comment talking about settings.cfg Task #5896: Do not use deprecated MyGUI properties + Task #6085: Replace boost::filesystem with std::filesystem + Task #6149: Dehardcode Lua API_REVISION + Task #6624: Drop support for saves made prior to 0.45 + Task #7048: Get rid of std::bind Task #7113: Move from std::atoi to std::from_char Task #7117: Replace boost::scoped_array with std::vector Task #7151: Do not use std::strerror to get errno error message + Task #7182: FFMpeg 5.1.1+ support Task #7394: Drop support for --fs-strict + Task #7720: Drop 360-degree screenshot support + Task #8141: Merge Instance Drop Modes + Task #8214: Drop script blacklisting functionality 0.48.0 ------ @@ -312,7 +533,6 @@ Feature #6700: Support windowed fullscreen Feature #6706: Save the size of the Options window Feature #6721: OpenMW-CS: Add option to open records in new window - Feature #6823: Animation layering for osgAnimation formats Feature #6867: Add a way to localize hardcoded strings in GUI Feature #6888: Add switch for armor degradation fix Feature #6925: Allow to use a mouse wheel to rotate a head in the race selection menu diff --git a/CI/Store-Symbols.ps1 b/CI/Store-Symbols.ps1 index 6328a2a2f6..53751213b8 100644 --- a/CI/Store-Symbols.ps1 +++ b/CI/Store-Symbols.ps1 @@ -1,12 +1,23 @@ +param ( + [switch] $SkipCompress +) + +$ErrorActionPreference = "Stop" + if (-Not (Test-Path CMakeCache.txt)) { Write-Error "This script must be run from the build directory." } -if (-Not (Test-Path .cmake\api\v1\reply)) +if (-Not (Test-Path .cmake\api\v1\reply\index-*.json) -Or -Not ((Get-Content -Raw .cmake\api\v1\reply\index-*.json | ConvertFrom-Json).reply.PSObject.Properties.Name -contains "codemodel-v2")) { + Write-Output "Running CMake query..." New-Item -Type File -Force .cmake\api\v1\query\codemodel-v2 cmake . + if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" + } + Write-Output "Done." } try @@ -43,10 +54,31 @@ finally if (-not (Test-Path symstore-venv)) { python -m venv symstore-venv + if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" + } } -if (-not (Test-Path symstore-venv\Scripts\symstore.exe)) +$symstoreVersion = "0.3.4" +if (-not (Test-Path symstore-venv\Scripts\symstore.exe) -or -not ((symstore-venv\Scripts\pip show symstore | Select-String '(?<=Version: ).*').Matches.Value -eq $symstoreVersion)) { - symstore-venv\Scripts\pip install symstore==0.3.3 + symstore-venv\Scripts\pip install symstore==$symstoreVersion + if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" + } } + $artifacts = $artifacts | Where-Object { Test-Path $_ } -symstore-venv\Scripts\symstore --compress .\SymStore @artifacts + +Write-Output "Storing symbols..." + +$optionalArgs = @() +if (-not $SkipCompress) { + $optionalArgs += "--compress" +} + +symstore-venv\Scripts\symstore $optionalArgs --skip-published .\SymStore @artifacts +if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" +} + +Write-Output "Done." diff --git a/CI/before_install.linux.sh b/CI/before_install.linux.sh deleted file mode 100755 index eff3fd7196..0000000000 --- a/CI/before_install.linux.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -ex - -#sudo ln -sf /usr/bin/clang-6 /usr/local/bin/clang -#sudo ln -sf /usr/bin/clang++-6 /usr/local/bin/clang++ diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index a467b589da..0120c55202 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,41 +1,30 @@ #!/bin/sh -ex export HOMEBREW_NO_EMOJI=1 - -brew uninstall --ignore-dependencies python@3.8 || true -brew uninstall --ignore-dependencies python@3.9 || true -brew uninstall --ignore-dependencies qt@6 || true -brew uninstall --ignore-dependencies jpeg || true +export HOMEBREW_NO_INSTALL_CLEANUP=1 +export HOMEBREW_AUTOREMOVE=1 brew tap --repair brew update --quiet -# Some of these tools can come from places other than brew, so check before installing -brew reinstall xquartz fontconfig freetype harfbuzz brotli - -# Fix: can't open file: @loader_path/libbrotlicommon.1.dylib (No such file or directory) -BREW_LIB_PATH="$(brew --prefix)/lib" -install_name_tool -change "@loader_path/libbrotlicommon.1.dylib" "${BREW_LIB_PATH}/libbrotlicommon.1.dylib" ${BREW_LIB_PATH}/libbrotlidec.1.dylib -install_name_tool -change "@loader_path/libbrotlicommon.1.dylib" "${BREW_LIB_PATH}/libbrotlicommon.1.dylib" ${BREW_LIB_PATH}/libbrotlienc.1.dylib +brew install curl xquartz gd fontconfig freetype harfbuzz brotli command -v ccache >/dev/null 2>&1 || brew install ccache command -v cmake >/dev/null 2>&1 || brew install cmake command -v qmake >/dev/null 2>&1 || brew install qt@5 export PATH="/opt/homebrew/opt/qt@5/bin:$PATH" - # Install deps -brew install icu4c yaml-cpp sqlite +brew install openal-soft icu4c yaml-cpp sqlite ccache --version cmake --version qmake --version if [[ "${MACOS_AMD64}" ]]; then - curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20221113.zip -o ~/openmw-deps.zip + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802.zip -o ~/openmw-deps.zip + unzip -o ~/openmw-deps.zip -d /tmp > /dev/null else - curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20230722_arm64.zip -o ~/openmw-deps.zip + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240818-arm64.tar.xz -o ~/openmw-deps.tar.xz + tar xf ~/openmw-deps.tar.xz -C /tmp > /dev/null fi - -unzip -o ~/openmw-deps.zip -d /tmp > /dev/null - diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index 0edd38628f..2589c2807e 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -7,14 +7,6 @@ free -m # Silence a git warning git config --global advice.detachedHead false -BUILD_UNITTESTS=OFF -BUILD_BENCHMARKS=OFF - -if [[ "${BUILD_TESTS_ONLY}" ]]; then - BUILD_UNITTESTS=ON - BUILD_BENCHMARKS=ON -fi - # setup our basic cmake build options declare -a CMAKE_CONF_OPTS=( -DCMAKE_C_COMPILER="${CC:-/usr/bin/cc}" @@ -22,7 +14,7 @@ declare -a CMAKE_CONF_OPTS=( -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_INSTALL_PREFIX=install - -DBUILD_SHARED_LIBS=OFF + -DBUILD_SHARED_LIBS="${BUILD_SHARED_LIBS:-OFF}" -DUSE_SYSTEM_TINYXML=ON -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=ON -DOPENMW_CXX_FLAGS="-Werror -Werror=implicit-fallthrough" # flags specific to OpenMW project @@ -46,14 +38,14 @@ fi if [[ $CI_CLANG_TIDY ]]; then CMAKE_CONF_OPTS+=( - -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*" - -DBUILD_UNITTESTS=ON - -DBUILD_OPENCS_TESTS=ON - -DBUILD_BENCHMARKS=ON + -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*" + -DBUILD_COMPONENTS_TESTS=ON + -DBUILD_OPENMW_TESTS=ON + -DBUILD_OPENCS_TESTS=ON + -DBUILD_BENCHMARKS=ON ) fi - if [[ "${CMAKE_BUILD_TYPE}" ]]; then CMAKE_CONF_OPTS+=( -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} @@ -103,9 +95,10 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then -DBUILD_NAVMESHTOOL=OFF \ -DBUILD_BULLETOBJECTTOOL=OFF \ -DBUILD_NIFTEST=OFF \ - -DBUILD_UNITTESTS=${BUILD_UNITTESTS} \ - -DBUILD_OPENCS_TESTS=${BUILD_UNITTESTS} \ - -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \ + -DBUILD_COMPONENTS_TESTS=ON \ + -DBUILD_OPENMW_TESTS=ON \ + -DBUILD_OPENCS_TESTS=ON \ + -DBUILD_BENCHMARKS=ON \ .. elif [[ "${BUILD_OPENMW_ONLY}" ]]; then ${ANALYZE} cmake \ diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 2996a3d772..940c72c806 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -14,16 +14,6 @@ MISSINGTOOLS=0 command -v 7z >/dev/null 2>&1 || { echo "Error: 7z (7zip) is not on the path."; MISSINGTOOLS=1; } command -v cmake >/dev/null 2>&1 || { echo "Error: cmake (CMake) is not on the path."; MISSINGTOOLS=1; } -MISSINGPYTHON=0 -if ! command -v python >/dev/null 2>&1; then - echo "Warning: Python is not on the path, automatic Qt installation impossible." - MISSINGPYTHON=1 -elif ! python --version >/dev/null 2>&1; then - echo "Warning: Python is (probably) fake stub Python that comes bundled with newer versions of Windows, automatic Qt installation impossible." - echo "If you think you have Python installed, try changing the order of your PATH environment variable in Advanced System Settings." - MISSINGPYTHON=1 -fi - if [ $MISSINGTOOLS -ne 0 ]; then wrappedExit 1 fi @@ -54,7 +44,6 @@ function unixPathAsWindows { fi } -APPVEYOR=${APPVEYOR:-} CI=${CI:-} STEP=${STEP:-} @@ -72,10 +61,8 @@ PDBS="" PLATFORM="" CONFIGURATIONS=() TEST_FRAMEWORK="" -GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." BUILD_BENCHMARKS="" -OSG_MULTIVIEW_BUILD="" USE_WERROR="" USE_CLANG_TIDY="" @@ -144,9 +131,6 @@ while [ $# -gt 0 ]; do b ) BUILD_BENCHMARKS=true ;; - M ) - OSG_MULTIVIEW_BUILD=true ;; - E ) USE_WERROR=true ;; @@ -221,16 +205,8 @@ if [ -z $VERBOSE ]; then STRIP="> /dev/null 2>&1" fi -if [ -z $APPVEYOR ]; then - echo "Running prebuild outside of Appveyor." - - DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}") - cd $(dirname "$DIR")/.. -else - echo "Running prebuild in Appveyor." - - cd "$APPVEYOR_BUILD_FOLDER" -fi +DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}") +cd $(dirname "$DIR")/.. run_cmd() { CMD="$1" @@ -241,13 +217,7 @@ run_cmd() { eval $CMD $@ > output.log 2>&1 || RET=$? if [ $RET -ne 0 ]; then - if [ -z $APPVEYOR ]; then - echo "Command $CMD failed, output can be found in $(real_pwd)/output.log" - else - echo - echo "Command $CMD failed;" - cat output.log - fi + echo "Command $CMD failed, output can be found in $(real_pwd)/output.log" else rm output.log fi @@ -304,6 +274,20 @@ download() { fi } +MANIFEST_FILE="" +download_from_manifest() { + if [ $# -ne 1 ]; then + echo "Invalid parameters to download_from_manifest." + return 1 + fi + { read -r URL && read -r HASH FILE; } < $1 + if [ -z $SKIP_DOWNLOAD ]; then + download "${FILE:?}" "${URL:?}" "${FILE:?}" + fi + echo "${HASH:?} ${FILE:?}" | sha512sum --check + MANIFEST_FILE="${FILE:?}" +} + real_pwd() { if type cygpath >/dev/null 2>&1; then cygpath -am "$PWD" @@ -357,6 +341,26 @@ add_qt_style_dlls() { QT_STYLES[$CONFIG]="${QT_STYLES[$CONFIG]} $@" } +declare -A QT_IMAGEFORMATS +QT_IMAGEFORMATS["Release"]="" +QT_IMAGEFORMATS["Debug"]="" +QT_IMAGEFORMATS["RelWithDebInfo"]="" +add_qt_image_dlls() { + local CONFIG=$1 + shift + QT_IMAGEFORMATS[$CONFIG]="${QT_IMAGEFORMATS[$CONFIG]} $@" +} + +declare -A QT_ICONENGINES +QT_ICONENGINES["Release"]="" +QT_ICONENGINES["Debug"]="" +QT_ICONENGINES["RelWithDebInfo"]="" +add_qt_icon_dlls() { + local CONFIG=$1 + shift + QT_ICONENGINES[$CONFIG]="${QT_ICONENGINES[$CONFIG]} $@" +} + if [ -z $PLATFORM ]; then PLATFORM="$(uname -m)" fi @@ -368,38 +372,20 @@ fi case $VS_VERSION in 17|17.0|2022 ) GENERATOR="Visual Studio 17 2022" - TOOLSET="vc143" + MSVC_TOOLSET="vc143" MSVC_REAL_VER="17" - MSVC_VER="14.3" MSVC_DISPLAY_YEAR="2022" - OSG_MSVC_YEAR="2019" - MYGUI_MSVC_YEAR="2019" - LUA_MSVC_YEAR="2019" QT_MSVC_YEAR="2019" - BULLET_MSVC_YEAR="2019" - - BOOST_VER="1.80.0" - BOOST_VER_URL="1_80_0" - BOOST_VER_SDK="108000" ;; 16|16.0|2019 ) GENERATOR="Visual Studio 16 2019" - TOOLSET="vc142" + MSVC_TOOLSET="vc142" MSVC_REAL_VER="16" - MSVC_VER="14.2" MSVC_DISPLAY_YEAR="2019" - OSG_MSVC_YEAR="2019" - MYGUI_MSVC_YEAR="2019" - LUA_MSVC_YEAR="2019" QT_MSVC_YEAR="2019" - BULLET_MSVC_YEAR="2019" - - BOOST_VER="1.80.0" - BOOST_VER_URL="1_80_0" - BOOST_VER_SDK="108000" ;; 15|15.0|2017 ) @@ -534,42 +520,37 @@ if [ -n "$SINGLE_CONFIG" ]; then add_cmake_opts "-DCMAKE_BUILD_TYPE=${CONFIGURATIONS[0]}" fi -if ! [ -z $UNITY_BUILD ]; then +if [[ -n "$UNITY_BUILD" ]]; then add_cmake_opts "-DOPENMW_UNITY_BUILD=True" fi -if ! [ -z $USE_CCACHE ]; then - add_cmake_opts "-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" +if [ -n "$USE_CCACHE" ]; then + if [ -n "$NMAKE" ] || [ -n "$NINJA" ]; then + add_cmake_opts "-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DPRECOMPILE_HEADERS_WITH_MSVC=OFF" + else + echo "Ignoring -C (CCache) as it is incompatible with Visual Studio CMake generators" + fi fi # turn on LTO by default add_cmake_opts "-DOPENMW_LTO_BUILD=True" -if ! [ -z "$USE_WERROR" ]; then +if [[ -n "$USE_WERROR" ]]; then add_cmake_opts "-DOPENMW_MSVC_WERROR=ON" fi -if ! [ -z "$USE_CLANG_TIDY" ]; then +if [[ -n "$USE_CLANG_TIDY" ]]; then add_cmake_opts "-DCMAKE_CXX_CLANG_TIDY=\"clang-tidy --warnings-as-errors=*\"" fi -BULLET_VER="2.89" -FFMPEG_VER="4.2.2" -ICU_VER="70_1" -LUAJIT_VER="v2.1.0-beta3-452-g7a0cf5fd" -LZ4_VER="1.9.2" -OPENAL_VER="1.23.0" -QT_VER="5.15.2" - -OSG_ARCHIVE_NAME="OSGoS 3.6.5" -OSG_ARCHIVE="OSGoS-3.6.5-123-g68c5c573d-msvc${OSG_MSVC_YEAR}-win${BITS}" -OSG_ARCHIVE_REPO_URL="https://gitlab.com/OpenMW/openmw-deps/-/raw/main" -if ! [ -z $OSG_MULTIVIEW_BUILD ]; then - OSG_ARCHIVE_NAME="OSG-3.6-multiview" - OSG_ARCHIVE="OSG-3.6-multiview-d2ee5aa8-msvc${OSG_MSVC_YEAR}-win${BITS}" - OSG_ARCHIVE_REPO_URL="https://gitlab.com/madsbuvi/openmw-deps/-/raw/openmw-vr-ovr_multiview" -fi +QT_VER='6.6.3' +AQT_VERSION='v3.1.15' +VCPKG_TAG="2024-11-10" +VCPKG_PATH="vcpkg-x64-${VS_VERSION:?}-${VCPKG_TAG:?}" +VCPKG_PDB_PATH="vcpkg-x64-${VS_VERSION:?}-pdb-${VCPKG_TAG:?}" +VCPKG_MANIFEST="${VCPKG_PATH:?}.txt" +VCPKG_PDB_MANIFEST="${VCPKG_PDB_PATH:?}.txt" echo echo "===================================" @@ -577,7 +558,6 @@ echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}" echo "===================================" echo -# cd OpenMW/AppVeyor-test mkdir -p deps cd deps @@ -587,75 +567,17 @@ if [ -z $SKIP_DOWNLOAD ]; then echo "Downloading dependency packages." echo - # Boost - if [ -z $APPVEYOR ]; then - download "Boost ${BOOST_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \ - "boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" + DEPS_BASE_URL="https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows" + + download "${VCPKG_MANIFEST:?}" \ + "${DEPS_BASE_URL}/${VCPKG_MANIFEST:?}" \ + "${VCPKG_MANIFEST:?}" + + if [ -n "${VCPKG_PDB_MANIFEST:?}" ]; then + download "${VCPKG_PDB_PATH:?}" \ + "${DEPS_BASE_URL}/${VCPKG_PDB_MANIFEST:?}" \ + "${VCPKG_PDB_MANIFEST:?}" fi - - # Bullet - download "Bullet ${BULLET_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt.7z" \ - "Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt.7z" - - # FFmpeg - download "FFmpeg ${FFMPEG_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-${FFMPEG_VER}-win${BITS}.zip" \ - "ffmpeg-${FFMPEG_VER}-win${BITS}.zip" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-${FFMPEG_VER}-dev-win${BITS}.zip" \ - "ffmpeg-${FFMPEG_VER}-dev-win${BITS}.zip" - - # MyGUI - download "MyGUI 3.4.2" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}.7z" \ - "MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}.7z" - - if [ -n "$PDBS" ]; then - download "MyGUI symbols" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}-sym.7z" \ - "MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}-sym.7z" - fi - - # OpenAL - download "OpenAL-Soft ${OPENAL_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OpenAL-Soft-${OPENAL_VER}.zip" \ - "OpenAL-Soft-${OPENAL_VER}.zip" - - # OSGoS - download "${OSG_ARCHIVE_NAME}" \ - "${OSG_ARCHIVE_REPO_URL}/windows/${OSG_ARCHIVE}.7z" \ - "${OSG_ARCHIVE}.7z" - - if [ -n "$PDBS" ]; then - download "${OSG_ARCHIVE_NAME} symbols" \ - "${OSG_ARCHIVE_REPO_URL}/windows/${OSG_ARCHIVE}-sym.7z" \ - "${OSG_ARCHIVE}-sym.7z" - fi - - # SDL2 - download "SDL 2.24.0" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/SDL2-devel-2.24.0-VC.zip" \ - "SDL2-devel-2.24.0-VC.zip" - - # LZ4 - download "LZ4 ${LZ4_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/lz4_win${BITS}_v${LZ4_VER//./_}.7z" \ - "lz4_win${BITS}_v${LZ4_VER//./_}.7z" - - # LuaJIT - download "LuaJIT ${LUAJIT_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/LuaJIT-${LUAJIT_VER}-msvc${LUA_MSVC_YEAR}-win${BITS}.7z" \ - "LuaJIT-${LUAJIT_VER}-msvc${LUA_MSVC_YEAR}-win${BITS}.7z" - - # ICU - download "ICU ${ICU_VER/_/.}"\ - "https://github.com/unicode-org/icu/releases/download/release-${ICU_VER/_/-}/icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip" \ - "icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip" - - download "zlib 1.2.11"\ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/zlib-1.2.11-msvc2017-win64.7z" \ - "zlib-1.2.11-msvc2017-win64.7z" fi cd .. #/.. @@ -691,186 +613,47 @@ echo "Extracting dependencies, this might take a while..." echo "---------------------------------------------------" echo - -if [ -z $APPVEYOR ]; then - printf "Boost ${BOOST_VER}... " -else - printf "Boost ${BOOST_VER} AppVeyor... " -fi +cd $DEPS +echo +printf "vcpkg packages ${VCPKG_TAG:?}... " { - if [ -z $APPVEYOR ]; then - cd $DEPS_INSTALL - - BOOST_SDK="$(real_pwd)/Boost" - - # Boost's installer is still based on ms-dos API that doesn't support larger than 260 char path names - # We work around this by installing to root of the current working drive and then move it to our deps - # get the current working drive's root, we'll install to that temporarily - CWD_DRIVE_ROOT="$(powershell -command '(get-location).Drive.Root')Boost_temp" - CWD_DRIVE_ROOT_BASH=$(windowsPathAsUnix "$CWD_DRIVE_ROOT") - if [ -d CWD_DRIVE_ROOT_BASH ]; then - printf "Cannot continue, ${CWD_DRIVE_ROOT_BASH} aka ${CWD_DRIVE_ROOT} already exists. Please remove before re-running. "; - wrappedExit 1; - fi - - if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION ${BOOST_VER_SDK}" Boost/boost/version.hpp > /dev/null; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf Boost - CI_EXTRA_INNO_OPTIONS="" - [ -n "$CI" ] && CI_EXTRA_INNO_OPTIONS="//SUPPRESSMSGBOXES //LOG='boost_install.log'" - "${DEPS}/boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS} - mv "${CWD_DRIVE_ROOT_BASH}" "${BOOST_SDK}" - fi - add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ - -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}" - add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" - echo Done. + if [[ -d "${VCPKG_PATH:?}" ]]; then + printf "Exists. " else - # Appveyor has all the boost we need already - BOOST_SDK="c:/Libraries/boost_${BOOST_VER_URL}" + download_from_manifest "${VCPKG_MANIFEST:?}" + eval 7z x -y -o"${VCPKG_PATH:?}" "${MANIFEST_FILE:?}" ${STRIP} + fi + if [ -n "${PDBS}" ]; then + if [[ -d "${VCPKG_PDB_PATH:?}" ]]; then + printf "PDB exists. " + else + download_from_manifest "${VCPKG_PDB_MANIFEST:?}" + eval 7z x -y -o"${VCPKG_PDB_PATH:?}" "${MANIFEST_FILE:?}" ${STRIP} + fi + fi - add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ - -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.1" - add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" + add_cmake_opts -DCMAKE_TOOLCHAIN_FILE="$(real_pwd)/${VCPKG_PATH:?}/scripts/buildsystems/vcpkg.cmake" + add_cmake_opts -DLuaJit_INCLUDE_DIR="$(real_pwd)/${VCPKG_PATH:?}/installed/x64-windows/include/luajit" + add_cmake_opts -DLuaJit_LIBRARY="$(real_pwd)/${VCPKG_PATH:?}/installed/x64-windows/lib/lua51.lib" - echo Done. - fi -} -cd $DEPS -echo -printf "Bullet ${BULLET_VER}... " -{ - cd $DEPS_INSTALL - if [ -d Bullet ]; then - printf -- "Exists. (No version checking) " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf Bullet - eval 7z x -y "${DEPS}/Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt.7z" $STRIP - mv "Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt" Bullet - fi - add_cmake_opts -DBULLET_ROOT="$(real_pwd)/Bullet" - echo Done. -} -cd $DEPS -echo -printf "FFmpeg ${FFMPEG_VER}... " -{ - cd $DEPS_INSTALL - if [ -d FFmpeg ] && grep "${FFMPEG_VER}" FFmpeg/README.txt > /dev/null; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf FFmpeg - eval 7z x -y "${DEPS}/ffmpeg-${FFMPEG_VER}-win${BITS}.zip" $STRIP - eval 7z x -y "${DEPS}/ffmpeg-${FFMPEG_VER}-dev-win${BITS}.zip" $STRIP - mv "ffmpeg-${FFMPEG_VER}-win${BITS}-shared" FFmpeg - cp -r "ffmpeg-${FFMPEG_VER}-win${BITS}-dev/"* FFmpeg/ - rm -rf "ffmpeg-${FFMPEG_VER}-win${BITS}-dev" - fi - export FFMPEG_HOME="$(real_pwd)/FFmpeg" - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/FFmpeg/bin/"{avcodec-58,avformat-58,avutil-56,swresample-3,swscale-5}.dll - done - if [ $BITS -eq 32 ]; then - add_cmake_opts "-DCMAKE_EXE_LINKER_FLAGS=\"/machine:X86 /safeseh:no\"" - fi - echo Done. -} -cd $DEPS -echo -printf "MyGUI 3.4.2... " -{ - cd $DEPS_INSTALL - if [ -d MyGUI ] && \ - grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ - grep "MYGUI_VERSION_MINOR 4" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ - grep "MYGUI_VERSION_PATCH 2" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null - then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf MyGUI - eval 7z x -y "${DEPS}/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}-sym.7z" $STRIP - mv "MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}" MyGUI - fi - export MYGUI_HOME="$(real_pwd)/MyGUI" for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="_d" - MYGUI_CONFIGURATION="Debug" + if [[ ${CONFIGURATION:?} == "Debug" ]]; then + VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/debug/bin" + + add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Debug/MyGUIEngine_d.dll" else - SUFFIX="" - MYGUI_CONFIGURATION="RelWithDebInfo" - fi - add_runtime_dlls $CONFIGURATION "$(pwd)/MyGUI/bin/${MYGUI_CONFIGURATION}/MyGUIEngine${SUFFIX}.dll" - done - echo Done. -} -cd $DEPS -echo -printf "OpenAL-Soft ${OPENAL_VER}... " -{ - if [ -d openal-soft-${OPENAL_VER}-bin ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf openal-soft-${OPENAL_VER}-bin - eval 7z x -y OpenAL-Soft-${OPENAL_VER}.zip $STRIP - fi - OPENAL_SDK="$(real_pwd)/openal-soft-${OPENAL_VER}-bin" - add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \ - -DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib" - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/openal-soft-${OPENAL_VER}-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll" - done - echo Done. -} -cd $DEPS -echo -printf "${OSG_ARCHIVE_NAME}... " -{ - cd $DEPS_INSTALL - if [ -d OSG ] && \ - grep "OPENSCENEGRAPH_MAJOR_VERSION 3" OSG/include/osg/Version > /dev/null && \ - grep "OPENSCENEGRAPH_MINOR_VERSION 6" OSG/include/osg/Version > /dev/null && \ - grep "OPENSCENEGRAPH_PATCH_VERSION 5" OSG/include/osg/Version > /dev/null - then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf OSG - eval 7z x -y "${DEPS}/${OSG_ARCHIVE}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/${OSG_ARCHIVE}-sym.7z" $STRIP - mv "${OSG_ARCHIVE}" OSG - fi - OSG_SDK="$(real_pwd)/OSG" - add_cmake_opts -DOSG_DIR="$OSG_SDK" - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="d" - SUFFIX_UPCASE="D" - else - SUFFIX="" - SUFFIX_UPCASE="" + VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/bin" + + add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Release/MyGUIEngine.dll" fi - if ! [ -z $OSG_MULTIVIEW_BUILD ]; then - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{ot21-OpenThreads,libpng16}${SUFFIX}.dll \ - "$(pwd)/OSG/bin/osg162-osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow,Sim}${SUFFIX}.dll - else - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,icuuc58,libpng16}${SUFFIX}.dll \ - "$(pwd)/OSG/bin/libxml2"${SUFFIX_UPCASE}.dll \ - "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow,Sim}${SUFFIX}.dll - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/icudt58.dll" - if [ $CONFIGURATION == "Debug" ]; then - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{boost_system-vc141-mt-gd-1_63,collada-dom2.4-dp-vc141-mt-d}.dll - else - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{boost_system-vc141-mt-1_63,collada-dom2.4-dp-vc141-mt}.dll - fi - fi - add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dae,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll - add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll + add_osg_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/osgPlugins-3.6.5/*.dll" + add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/*.dll" done + echo Done. } + cd $DEPS echo printf "Qt ${QT_VER}... " @@ -889,30 +672,11 @@ printf "Qt ${QT_VER}... " if [ -d "Qt/${QT_VER}" ]; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then - if [ $MISSINGPYTHON -ne 0 ]; then - echo "Can't be automatically installed without Python." - wrappedExit 1 - fi - pushd "$DEPS" > /dev/null - if ! [ -d 'aqt-venv' ]; then - echo " Creating Virtualenv for aqt..." - run_cmd python -m venv aqt-venv - fi - if [ -d 'aqt-venv/bin' ]; then - VENV_BIN_DIR='bin' - elif [ -d 'aqt-venv/Scripts' ]; then - VENV_BIN_DIR='Scripts' - else - echo "Error: Failed to create virtualenv in expected location." - wrappedExit 1 - fi - - # check version - aqt-venv/${VENV_BIN_DIR}/pip list | grep 'aqtinstall\s*1.1.3' || [ $? -ne 0 ] - if [ $? -eq 0 ]; then - echo " Installing aqt wheel into virtualenv..." - run_cmd "aqt-venv/${VENV_BIN_DIR}/pip" install aqtinstall==1.1.3 + if ! [ -f "aqt_x64-${AQT_VERSION}.exe" ]; then + download "aqt ${AQT_VERSION}"\ + "https://github.com/miurahr/aqtinstall/releases/download/${AQT_VERSION}/aqt_x64.exe" \ + "aqt_x64-${AQT_VERSION}.exe" fi popd > /dev/null @@ -921,7 +685,7 @@ printf "Qt ${QT_VER}... " mkdir Qt cd Qt - run_cmd "${DEPS}/aqt-venv/${VENV_BIN_DIR}/aqt" install ${QT_VER} windows desktop "win${BITS}_msvc${QT_MSVC_YEAR}${SUFFIX}" + run_cmd "${DEPS}/aqt_x64-${AQT_VERSION}.exe" install-qt windows desktop ${QT_VER} "win${BITS}_msvc${QT_MSVC_YEAR}${SUFFIX}" printf " Cleaning up extraneous data... " rm -rf Qt/{aqtinstall.log,Tools} @@ -929,129 +693,39 @@ printf "Qt ${QT_VER}... " echo Done. fi + QT_MAJOR_VER=$(echo "${QT_VER}" | awk -F '[.]' '{printf "%d", $1}') + QT_MINOR_VER=$(echo "${QT_VER}" | awk -F '[.]' '{printf "%d", $2}') + cd $QT_SDK - add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ - -DCMAKE_PREFIX_PATH="$QT_SDK" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then DLLSUFFIX="d" else DLLSUFFIX="" fi - if [ "${QT_VER:0:1}" -eq "6" ]; then - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_VER:0:1}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets}${DLLSUFFIX}.dll + + if [ "${QT_MAJOR_VER}" -eq 6 ]; then + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll + + # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" + if [ "${QT_MINOR_VER}" -ge 7 ]; then + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" + else + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" + fi else - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_VER:0:1}"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" fi + add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" - done - echo Done. -} -cd $DEPS -echo -printf "SDL 2.24.0... " -{ - if [ -d SDL2-2.24.0 ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf SDL2-2.24.0 - eval 7z x -y SDL2-devel-2.24.0-VC.zip $STRIP - fi - export SDL2DIR="$(real_pwd)/SDL2-2.24.0" - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/SDL2-2.24.0/lib/x${ARCHSUFFIX}/SDL2.dll" - done - echo Done. -} -cd $DEPS -echo -printf "LZ4 ${LZ4_VER}... " -{ - if [ -d LZ4_${LZ4_VER} ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf LZ4_${LZ4_VER} - eval 7z x -y lz4_win${BITS}_v${LZ4_VER//./_}.7z -o$(real_pwd)/LZ4_${LZ4_VER} $STRIP - fi - export LZ4DIR="$(real_pwd)/LZ4_${LZ4_VER}" - add_cmake_opts -DLZ4_INCLUDE_DIR="${LZ4DIR}/include" \ - -DLZ4_LIBRARY="${LZ4DIR}/lib/liblz4.lib" - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - LZ4_CONFIGURATION="Debug" - else - SUFFIX="" - LZ4_CONFIGURATION="Release" - fi - add_runtime_dlls $CONFIGURATION "$(pwd)/LZ4_${LZ4_VER}/bin/${LZ4_CONFIGURATION}/liblz4.dll" - done - echo Done. -} -cd $DEPS -echo -printf "LuaJIT ${LUAJIT_VER}... " -{ - if [ -d LuaJIT ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf LuaJIT - eval 7z x -y LuaJIT-${LUAJIT_VER}-msvc${LUA_MSVC_YEAR}-win${BITS}.7z -o$(real_pwd)/LuaJIT $STRIP - fi - export LUAJIT_DIR="$(real_pwd)/LuaJIT" - add_cmake_opts -DLuaJit_INCLUDE_DIR="${LUAJIT_DIR}/include" \ - -DLuaJit_LIBRARY="${LUAJIT_DIR}/lib/lua51.lib" - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $CONFIGURATION "$(pwd)/LuaJIT/bin/lua51.dll" + add_qt_image_dlls $CONFIGURATION "$(pwd)/plugins/imageformats/qsvg${DLLSUFFIX}.dll" + add_qt_icon_dlls $CONFIGURATION "$(pwd)/plugins/iconengines/qsvgicon${DLLSUFFIX}.dll" done echo Done. } -cd $DEPS -echo -printf "ICU ${ICU_VER/_/.}... " -{ - if [ -d ICU-${ICU_VER} ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf ICU-${ICU_VER} - eval 7z x -y icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip -o$(real_pwd)/ICU-${ICU_VER} $STRIP - fi - ICU_ROOT="$(real_pwd)/ICU-${ICU_VER}" - add_cmake_opts -DICU_ROOT="${ICU_ROOT}" \ - -DICU_INCLUDE_DIR="${ICU_ROOT}/include" \ - -DICU_I18N_LIBRARY="${ICU_ROOT}/lib${BITS}/icuin.lib " \ - -DICU_UC_LIBRARY="${ICU_ROOT}/lib${BITS}/icuuc.lib " \ - -DICU_DEBUG=ON - - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/ICU-${ICU_VER}/bin${BITS}/icudt${ICU_VER/_*/}.dll" - add_runtime_dlls $config "$(pwd)/ICU-${ICU_VER}/bin${BITS}/icuin${ICU_VER/_*/}.dll" - add_runtime_dlls $config "$(pwd)/ICU-${ICU_VER}/bin${BITS}/icuuc${ICU_VER/_*/}.dll" - done - echo Done. -} - -cd $DEPS -echo -printf "zlib 1.2.11... " -{ - if [ -d zlib-1.2.11-msvc2017-win64 ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf zlib-1.2.11-msvc2017-win64 - eval 7z x -y zlib-1.2.11-msvc2017-win64.7z $STRIP - fi - add_cmake_opts -DZLIB_ROOT="$(real_pwd)/zlib-1.2.11-msvc2017-win64" - for config in ${CONFIGURATIONS[@]}; do - if [ $config == "Debug" ]; then - add_runtime_dlls $config "$(pwd)/zlib-1.2.11-msvc2017-win64/bin/zlibd.dll" - else - add_runtime_dlls $config "$(pwd)/zlib-1.2.11-msvc2017-win64/bin/zlib.dll" - fi - done - echo Done. -} +add_cmake_opts -DCMAKE_PREFIX_PATH="\"${QT_SDK}\"" echo cd $DEPS_INSTALL/.. @@ -1137,6 +811,20 @@ fi cp "$DLL" "${DLL_PREFIX}styles" done echo + echo "- Qt Image Format DLLs..." + mkdir -p ${DLL_PREFIX}imageformats + for DLL in ${QT_IMAGEFORMATS[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}imageformats" + done + echo + echo "- Qt Icon Engine DLLs..." + mkdir -p ${DLL_PREFIX}iconengines + for DLL in ${QT_ICONENGINES[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}iconengines" + done + echo done #fi @@ -1145,8 +833,9 @@ if [ "${BUILD_BENCHMARKS}" ]; then fi if [ -n "${TEST_FRAMEWORK}" ]; then - add_cmake_opts -DBUILD_UNITTESTS=ON + add_cmake_opts -DBUILD_COMPONENTS_TESTS=ON add_cmake_opts -DBUILD_OPENCS_TESTS=ON + add_cmake_opts -DBUILD_OPENMW_TESTS=ON fi if [ -n "$ACTIVATE_MSVC" ]; then diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index c956f27514..9f7a5bde8f 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -1,30 +1,46 @@ #!/bin/sh -e -export CXX=clang++ -export CC=clang - # Silence a git warning git config --global advice.detachedHead false +rm -fr build +mkdir build +cd build + DEPENDENCIES_ROOT="/tmp/openmw-deps" QT_PATH=$(brew --prefix qt@5) ICU_PATH=$(brew --prefix icu4c) +OPENAL_PATH=$(brew --prefix openal-soft) CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache -mkdir build -cd build + +declare -a CMAKE_CONF_OPTS=( +-D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH;$OPENAL_PATH" +-D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" +-D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" +-D CMAKE_CXX_FLAGS="-stdlib=libc++" +-D CMAKE_C_COMPILER="clang" +-D CMAKE_CXX_COMPILER="clang++" +-D CMAKE_OSX_DEPLOYMENT_TARGET="13.6" +-D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE +-D Boost_INCLUDE_DIR="$DEPENDENCIES_ROOT/include" +-D OSGPlugins_LIB_DIR="$DEPENDENCIES_ROOT/lib/osgPlugins-3.6.5" +-D ICU_ROOT="$ICU_PATH" +-D OPENMW_OSX_DEPLOYMENT=TRUE +) + +if [[ "${CMAKE_BUILD_TYPE}" ]]; then + CMAKE_CONF_OPTS+=( + -D CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ) +else + CMAKE_CONF_OPTS+=( + -D CMAKE_BUILD_TYPE=RelWithDebInfo + ) +fi cmake \ --D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \ --D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ --D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ --D CMAKE_CXX_FLAGS="-stdlib=libc++" \ --D CMAKE_C_FLAGS_RELEASE="-g -O0" \ --D CMAKE_CXX_FLAGS_RELEASE="-g -O0" \ --D CMAKE_OSX_DEPLOYMENT_TARGET="11" \ --D CMAKE_BUILD_TYPE=RELEASE \ --D OPENMW_OSX_DEPLOYMENT=TRUE \ --D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE \ +"${CMAKE_CONF_OPTS[@]}" \ -D BUILD_OPENMW=TRUE \ -D BUILD_OPENCS=TRUE \ -D BUILD_ESMTOOL=TRUE \ @@ -33,6 +49,5 @@ cmake \ -D BUILD_NIFTEST=TRUE \ -D BUILD_NAVMESHTOOL=TRUE \ -D BUILD_BULLETOBJECTTOOL=TRUE \ --D ICU_ROOT="$ICU_PATH" \ -G"Unix Makefiles" \ .. diff --git a/CI/build.msvc.sh b/CI/build.msvc.sh deleted file mode 100644 index eac969b0d4..0000000000 --- a/CI/build.msvc.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash - -APPVEYOR="" -CI="" - -PACKAGE="" -PLATFORM="" -CONFIGURATION="" -VS_VERSION="" - -if [ -z $PLATFORM ]; then - PLATFORM=`uname -m` -fi - -if [ -z $CONFIGURATION ]; then - CONFIGURATION="Debug" -fi - -case $VS_VERSION in - 14|14.0|2015 ) - GENERATOR="Visual Studio 14 2015" - MSVC_YEAR="2015" - MSVC_VER="14.0" - ;; - -# 12|2013| - * ) - GENERATOR="Visual Studio 12 2013" - MSVC_YEAR="2013" - MVSC_VER="12.0" - ;; -esac - -case $PLATFORM in - x64|x86_64|x86-64|win64|Win64 ) - BITS=64 - ;; - - x32|x86|i686|i386|win32|Win32 ) - BITS=32 - ;; -esac - -case $CONFIGURATION in - debug|Debug|DEBUG ) - CONFIGURATION=Debug - ;; - - release|Release|RELEASE ) - CONFIGURATION=Release - ;; - - relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) - CONFIGURATION=RelWithDebInfo - ;; -esac - -if [ -z $APPVEYOR ]; then - echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build outside of Appveyor." - - DIR=$(echo "$0" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1,") - cd $(dirname "$DIR")/.. -else - echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build in Appveyor." - - cd $APPVEYOR_BUILD_FOLDER -fi - -BUILD_DIR="MSVC${MSVC_YEAR}_${BITS}" -cd ${BUILD_DIR} - -which msbuild > /dev/null -if [ $? -ne 0 ]; then - msbuild() { - /c/Program\ Files\ \(x86\)/MSBuild/${MSVC_VER}/Bin/MSBuild.exe "$@" - } -fi - -if [ -z $APPVEYOR ]; then - msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 -else - msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 //logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" -fi - -RET=$? -if [ $RET -eq 0 ] && [ ! -z $PACKAGE ]; then - msbuild PACKAGE.vcxproj //t:Build //m:8 - RET=$? -fi - -exit $RET diff --git a/CI/check_qt_translations.sh b/CI/check_qt_translations.sh new file mode 100755 index 0000000000..1fc2e19002 --- /dev/null +++ b/CI/check_qt_translations.sh @@ -0,0 +1,11 @@ +#!/bin/bash -ex + +set -o pipefail + +LUPDATE="${LUPDATE:-lupdate}" + +${LUPDATE:?} -locations none apps/wizard -ts files/lang/wizard_*.ts +${LUPDATE:?} -locations none apps/launcher -ts files/lang/launcher_*.ts +${LUPDATE:?} -locations none components/contentselector components/process -ts files/lang/components_*.ts + +! (git diff --name-only | grep -q "^") || (echo -e "\033[0;31mBuild a 'translations' CMake target to update Qt localization for these files:\033[0;0m"; git diff --name-only | xargs -i echo -e "\033[0;31m{}\033[0;0m"; exit -1) diff --git a/CI/check_tabs.sh b/CI/check_tabs.sh deleted file mode 100755 index 1e88b57fd4..0000000000 --- a/CI/check_tabs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -OUTPUT=$(grep -nRP '\t' --include=\*.{cpp,hpp,c,h} --exclude=ui_\* apps components) - -if [[ $OUTPUT ]] ; then - echo "Error: Tab characters found!" - echo $OUTPUT - exit 1 -fi diff --git a/CI/deploy.osx.sh b/CI/deploy.osx.sh deleted file mode 100755 index 88804d8587..0000000000 --- a/CI/deploy.osx.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -# This script expect the following environment variables to be set: -# - OSX_DEPLOY_KEY: private SSH key, must be encoded like this before adding it to Travis secrets: https://github.com/travis-ci/travis-ci/issues/7715#issuecomment-433301692 -# - OSX_DEPLOY_HOST: string specifying SSH of the following format: ssh-user@ssh-host -# - OSX_DEPLOY_PORT: SSH port, it can't be a part of the host string because scp doesn't accept hosts with ports -# - OSX_DEPLOY_HOST_FINGERPRINT: fingerprint of the host, can be obtained by using ssh-keygen -F [host]:port & putting it in double quotes when adding to Travis secrets - -SSH_KEY_PATH="$HOME/.ssh/openmw_deploy" -REMOTE_PATH="\$HOME/nightly" - -echo "$OSX_DEPLOY_KEY" > "$SSH_KEY_PATH" -chmod 600 "$SSH_KEY_PATH" -echo "$OSX_DEPLOY_HOST_FINGERPRINT" >> "$HOME/.ssh/known_hosts" - -cd build || exit 1 - -DATE=$(date +'%d%m%Y') -SHORT_COMMIT=$(git rev-parse --short "${TRAVIS_COMMIT}") -TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg" - -if ! ssh -p "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" "$OSX_DEPLOY_HOST" "ls \"$REMOTE_PATH\"" | grep "$SHORT_COMMIT" > /dev/null; then - scp -P "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" ./*.dmg "$OSX_DEPLOY_HOST:$REMOTE_PATH/$TARGET_FILENAME" -else - echo "An existing nightly build for commit ${SHORT_COMMIT} has been found, skipping upload." -fi diff --git a/CI/file_name_exceptions.txt b/CI/file_name_exceptions.txt index 5035d73f27..14d106169b 100644 --- a/CI/file_name_exceptions.txt +++ b/CI/file_name_exceptions.txt @@ -8,26 +8,26 @@ apps/openmw/mwsound/sound_buffer.cpp apps/openmw/mwsound/sound_buffer.hpp apps/openmw/mwsound/sound_decoder.hpp apps/openmw/mwsound/sound_output.hpp -apps/openmw_test_suite/esm/test_fixed_string.cpp -apps/openmw_test_suite/files/conversion_tests.cpp -apps/openmw_test_suite/lua/test_async.cpp -apps/openmw_test_suite/lua/test_configuration.cpp -apps/openmw_test_suite/lua/test_l10n.cpp -apps/openmw_test_suite/lua/test_lua.cpp -apps/openmw_test_suite/lua/test_scriptscontainer.cpp -apps/openmw_test_suite/lua/test_serialization.cpp -apps/openmw_test_suite/lua/test_storage.cpp -apps/openmw_test_suite/lua/test_ui_content.cpp -apps/openmw_test_suite/lua/test_utilpackage.cpp -apps/openmw_test_suite/misc/test_endianness.cpp -apps/openmw_test_suite/misc/test_resourcehelpers.cpp -apps/openmw_test_suite/misc/test_stringops.cpp -apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp -apps/openmw_test_suite/mwscript/test_scripts.cpp -apps/openmw_test_suite/mwscript/test_utils.hpp -apps/openmw_test_suite/mwworld/test_store.cpp -apps/openmw_test_suite/openmw_test_suite.cpp -apps/openmw_test_suite/testing_util.hpp +apps/components_tests/esm/test_fixed_string.cpp +apps/components_tests/files/conversion_tests.cpp +apps/components_tests/lua/test_async.cpp +apps/components_tests/lua/test_configuration.cpp +apps/components_tests/lua/test_l10n.cpp +apps/components_tests/lua/test_lua.cpp +apps/components_tests/lua/test_scriptscontainer.cpp +apps/components_tests/lua/test_serialization.cpp +apps/components_tests/lua/test_storage.cpp +apps/components_tests/lua/test_ui_content.cpp +apps/components_tests/lua/test_utilpackage.cpp +apps/components_tests/lua/test_inputactions.cpp +apps/components_tests/lua/test_yaml.cpp +apps/components_tests/misc/test_endianness.cpp +apps/components_tests/misc/test_resourcehelpers.cpp +apps/components_tests/misc/test_stringops.cpp +apps/openmw_tests/mwdialogue/test_keywordsearch.cpp +apps/openmw_tests/mwscript/test_scripts.cpp +apps/openmw_tests/mwscript/test_utils.hpp +apps/openmw_tests/mwworld/test_store.cpp components/bsa/bsa_file.cpp components/bsa/bsa_file.hpp components/crashcatcher/windows_crashcatcher.cpp diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 4417a04317..d29f16f55f 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -33,9 +33,10 @@ declare -rA GROUPED_DEPS=( libboost-system-dev libboost-iostreams-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-dev - libbullet-dev liblz4-dev libpng-dev libjpeg-dev libluajit-5.1-dev - librecast-dev libsqlite3-dev ca-certificates libicu-dev libyaml-cpp-dev + libsdl2-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libopenal-dev + libunshield-dev libtinyxml-dev libbullet-dev liblz4-dev libpng-dev libjpeg-dev + libluajit-5.1-dev librecast-dev libsqlite3-dev ca-certificates libicu-dev + libyaml-cpp-dev libqt5svg5 libqt5svg5-dev " # These dependencies can alternatively be built and linked statically. @@ -86,7 +87,7 @@ declare -rA GROUPED_DEPS=( libswresample3 libswscale5 libtinyxml2.6.2v5 - libyaml-cpp0.7 + libyaml-cpp0.8 python3-pip xvfb " @@ -99,6 +100,12 @@ declare -rA GROUPED_DEPS=( clang-format-14 git-core " + + [openmw-qt-translations]=" + qttools5-dev + qttools5-dev-tools + git-core + " ) if [[ $# -eq 0 ]]; then @@ -122,4 +129,7 @@ mkdir -pv "$APT_CACHE_DIR" apt-get update -yqq apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common gnupg >/dev/null add-apt-repository -y ppa:openmw/openmw +add-apt-repository -y ppa:openmw/openmw-daily +add-apt-repository -y ppa:openmw/staging apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null +apt list --installed diff --git a/CI/org.openmw.OpenMW.devel.yaml b/CI/org.openmw.OpenMW.devel.yaml index 9e8fb9eeb9..9f4d921cf1 100644 --- a/CI/org.openmw.OpenMW.devel.yaml +++ b/CI/org.openmw.OpenMW.devel.yaml @@ -125,8 +125,8 @@ modules: - "-DMYGUI_BUILD_PLUGINS=0" sources: - type: archive - url: https://github.com/MyGUI/mygui/archive/refs/tags/MyGUI3.4.2.tar.gz - sha256: 1cc45fb96c9438e3476778449af0378443d84794a458978a29c75306e45dd45a + url: https://github.com/MyGUI/mygui/archive/refs/tags/MyGUI3.4.3.tar.gz + sha256: 33c91b531993047e77cace36d6fea73634b8c17bd0ed193d4cd12ac7c6328abd - name: libunshield buildsystem: cmake-ninja diff --git a/CI/run_integration_tests.sh b/CI/run_integration_tests.sh index d7b025df52..e79408926a 100755 --- a/CI/run_integration_tests.sh +++ b/CI/run_integration_tests.sh @@ -1,6 +1,12 @@ #!/bin/bash -ex -git clone --depth=1 https://gitlab.com/OpenMW/example-suite.git +mkdir example-suite +cd example-suite +git init +git remote add origin https://gitlab.com/OpenMW/example-suite.git +git fetch --depth=1 origin ${EXAMPLE_SUITE_REVISION} +git checkout FETCH_HEAD +cd .. xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \ scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ diff --git a/CI/teal_ci.sh b/CI/teal_ci.sh index 5ea312e88c..6f5953f1be 100755 --- a/CI/teal_ci.sh +++ b/CI/teal_ci.sh @@ -1,14 +1,15 @@ -set -e +#!/bin/bash -e docs/source/install_luadocumentor_in_docker.sh PATH=$PATH:~/luarocks/bin pushd . echo "Install Teal Cyan" -git clone https://github.com/teal-language/cyan.git --depth 1 +git clone https://github.com/teal-language/cyan.git cd cyan -luarocks make cyan-dev-1.rockspec +git checkout v0.4.0 +luarocks make cyan-0.4.0-1.rockspec popd +cyan version scripts/generate_teal_declarations.sh ./teal_declarations -zip teal_declarations.zip -r teal_declarations diff --git a/CI/ubuntu_gcc_preprocess.sh b/CI/ubuntu_gcc_preprocess.sh index 05d7528e41..d519d178aa 100755 --- a/CI/ubuntu_gcc_preprocess.sh +++ b/CI/ubuntu_gcc_preprocess.sh @@ -31,7 +31,8 @@ cmake \ -D BUILD_OPENCS=ON \ -D BUILD_OPENCS_TESTS=ON \ -D BUILD_OPENMW=ON \ - -D BUILD_UNITTESTS=ON \ + -D BUILD_OPENMW_TESTS=ON \ + -D BUILD_COMPONENTS_TESTS=ON \ -D BUILD_WIZARD=ON \ "${SRC}" cmake --build . --parallel diff --git a/CMakeLists.txt b/CMakeLists.txt index d9865e6569..80a50297ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,14 @@ if(OPENMW_GL4ES_MANUAL_INIT) add_definitions(-DOPENMW_GL4ES_MANUAL_INIT) endif() +if (APPLE OR WIN32) + set(DEPLOY_QT_TRANSLATIONS_DEFAULT ON) +else () + set(DEPLOY_QT_TRANSLATIONS_DEFAULT OFF) +endif () + +option(DEPLOY_QT_TRANSLATIONS "Deploy standard Qt translations to resources folder. Needed when OpenMW applications are deployed with Qt libraries" ${DEPLOY_QT_TRANSLATIONS_DEFAULT}) + # Apps and tools option(BUILD_OPENMW "Build OpenMW" ON) option(BUILD_LAUNCHER "Build Launcher" ON) @@ -31,11 +39,13 @@ option(BUILD_ESMTOOL "Build ESM inspector" ON) option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) -option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) +option(BUILD_COMPONENTS_TESTS "Build components library tests" OFF) option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) option(BUILD_NAVMESHTOOL "Build navmesh tool" ON) option(BUILD_BULLETOBJECTTOOL "Build Bullet object tool" ON) option(BUILD_OPENCS_TESTS "Build OpenMW Construction Set tests" OFF) +option(BUILD_OPENMW_TESTS "Build OpenMW tests" OFF) +option(PRECOMPILE_HEADERS_WITH_MSVC "Precompile most common used headers with MSVC (alternative to ccache)" ON) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. @@ -54,6 +64,7 @@ IF(NOT CMAKE_BUILD_TYPE) ENDIF() if (APPLE) + set(CMAKE_FIND_FRAMEWORK LAST) # prefer dylibs over frameworks set(APP_BUNDLE_NAME "${CMAKE_PROJECT_NAME}.app") set(APP_BUNDLE_DIR "${OpenMW_BINARY_DIR}/${APP_BUNDLE_NAME}") @@ -71,6 +82,8 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) +set(OPENMW_LUA_API_REVISION 68) +set(OPENMW_POSTPROCESSING_API_REVISION 2) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") @@ -78,7 +91,7 @@ set(OPENMW_VERSION_COMMITDATE "") set(OPENMW_VERSION "${OPENMW_VERSION_MAJOR}.${OPENMW_VERSION_MINOR}.${OPENMW_VERSION_RELEASE}") -set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/stable/") +set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/") set(GIT_CHECKOUT FALSE) if(EXISTS ${PROJECT_SOURCE_DIR}/.git) @@ -111,8 +124,6 @@ include(WholeArchive) configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") -option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE) -option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE) option(QT_STATIC "Link static build of Qt into the binaries" FALSE) option(OPENMW_USE_SYSTEM_BULLET "Use system provided bullet physics library" ON) @@ -142,7 +153,7 @@ option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_stat option(OPENMW_USE_SYSTEM_RECASTNAVIGATION "Use system provided recastnavigation library" OFF) if(OPENMW_USE_SYSTEM_RECASTNAVIGATION) set(_recastnavigation_static_default OFF) - find_package(RecastNavigation REQUIRED) + find_package(RecastNavigation REQUIRED CONFIG) else() set(_recastnavigation_static_default ON) endif() @@ -156,13 +167,6 @@ option(OPENMW_USE_SYSTEM_GOOGLETEST "Use system Google Test library." OFF) option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE) option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF) -# what is necessary to build documentation -IF( BUILD_DOCS ) - # Builds the documentation. - FIND_PACKAGE( Sphinx REQUIRED ) - FIND_PACKAGE( Doxygen REQUIRED ) -ENDIF() - # OS X deployment option(OPENMW_OSX_DEPLOYMENT OFF) @@ -178,6 +182,24 @@ if (MSVC) # there should be no relevant downsides to having it on: # https://docs.microsoft.com/en-us/cpp/build/reference/bigobj-increase-number-of-sections-in-dot-obj-file add_compile_options(/bigobj) + + add_compile_options(/Zc:__cplusplus) + + if (CMAKE_CXX_COMPILER_LAUNCHER OR CMAKE_C_COMPILER_LAUNCHER) + if (CMAKE_GENERATOR MATCHES "Visual Studio") + message(STATUS "A compiler launcher was specified, but will be unused by the current generator (${CMAKE_GENERATOR})") + else() + foreach (config_lower ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER "${config_lower}" config) + if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache") + string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_${config} "${CMAKE_C_FLAGS_${config}}") + endif() + if (CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_${config} "${CMAKE_CXX_FLAGS_${config}}") + endif() + endforeach() + endif() + endif() endif() # Set up common paths @@ -205,7 +227,7 @@ else() endif(APPLE) if (WIN32) - option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON) + option(USE_DEBUG_CONSOLE "Whether a console should be displayed if OpenMW isn't launched from the command line. Does not affect the Release configuration." ON) endif() if(MSVC) @@ -213,16 +235,25 @@ if(MSVC) endif() # Dependencies -find_package(OpenGL REQUIRED) +if (APPLE) + # Force CMake to use the installed version of OpenGL on macOS + set(_SAVE_CMAKE_FIND_FRAMEWORK ${CMAKE_FIND_FRAMEWORK}) + set(CMAKE_FIND_FRAMEWORK ONLY) + find_package(OpenGL REQUIRED) + set(CMAKE_FIND_FRAMEWORK ${_SAVE_CMAKE_FIND_FRAMEWORK}) + unset(_SAVE_CMAKE_FIND_FRAMEWORK) +else() + find_package(OpenGL REQUIRED) +endif() find_package(LZ4 REQUIRED) if (USE_QT) find_package(QT REQUIRED COMPONENTS Core NAMES Qt6 Qt5) if (QT_VERSION_MAJOR VERSION_EQUAL 5) - find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL REQUIRED) + find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL LinguistTools Svg REQUIRED) else() - find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets REQUIRED) + find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED) endif() message(STATUS "Using Qt${QT_VERSION}") endif() @@ -276,7 +307,7 @@ if (OPENMW_USE_SYSTEM_YAML_CPP) find_package(yaml-cpp REQUIRED) endif() -if ((BUILD_UNITTESTS OR BUILD_OPENCS_TESTS) AND OPENMW_USE_SYSTEM_GOOGLETEST) +if ((BUILD_COMPONENTS_TESTS OR BUILD_OPENCS_TESTS OR BUILD_OPENMW_TESTS) AND OPENMW_USE_SYSTEM_GOOGLETEST) find_package(GTest 1.10 REQUIRED) find_package(GMock 1.10 REQUIRED) endif() @@ -293,8 +324,8 @@ find_package(FFmpeg REQUIRED COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMP if(FFmpeg_FOUND) SET(FFVER_OK TRUE) - # Can not detect FFmpeg version on Windows for now - if (NOT WIN32) + # Can not detect FFmpeg version on Windows or Android for now + if (NOT WIN32 AND NOT ANDROID) if(FFmpeg_AVFORMAT_VERSION VERSION_LESS "57.56.100") message(STATUS "libavformat is too old! (${FFmpeg_AVFORMAT_VERSION}, wanted 57.56.100)") set(FFVER_OK FALSE) @@ -346,11 +377,6 @@ endif() # Platform specific if (WIN32) - if(NOT MINGW) - set(Boost_USE_STATIC_LIBS ON) - add_definitions(-DBOOST_ALL_NO_LIB) - endif(NOT MINGW) - # Suppress WinMain(), provided by SDL add_definitions(-DSDL_MAIN_HANDLED) @@ -433,32 +459,14 @@ if(HAVE_MULTIVIEW) add_definitions(-DOSG_HAS_MULTIVIEW) endif(HAVE_MULTIVIEW) -set(BOOST_COMPONENTS system program_options iostreams) -if(WIN32) - set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) - if(MSVC) - # boost-zlib is not present (nor needed) in vcpkg version of boost. - # there, it is part of boost-iostreams instead. - set(BOOST_OPTIONAL_COMPONENTS zlib) - endif(MSVC) -endif(WIN32) +set(BOOST_COMPONENTS iostreams program_options system) -IF(BOOST_STATIC) - set(Boost_USE_STATIC_LIBS ON) -endif() - -set(Boost_NO_BOOST_CMAKE ON) - -find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS}) - -if(Boost_VERSION_STRING VERSION_GREATER_EQUAL 1.77.0) - find_package(Boost 1.77.0 REQUIRED COMPONENTS atomic) -endif() +find_package(Boost 1.70.0 CONFIG REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS}) if(OPENMW_USE_SYSTEM_MYGUI) - find_package(MyGUI 3.4.2 REQUIRED) + find_package(MyGUI 3.4.3 REQUIRED) endif() -find_package(SDL2 2.0.9 REQUIRED) +find_package(SDL2 2.0.10 REQUIRED) find_package(OpenAL REQUIRED) find_package(ZLIB REQUIRED) @@ -471,7 +479,7 @@ else(USE_LUAJIT) find_package(Lua REQUIRED) add_compile_definitions(NO_LUAJIT) endif(USE_LUAJIT) -if (NOT WIN32) +if (NOT WIN32 AND NOT ANDROID) include(cmake/CheckLuaCustomAllocator.cmake) endif() @@ -482,8 +490,6 @@ set(SOL_CONFIG_DIR ${OpenMW_SOURCE_DIR}/extern/sol_config) include_directories( BEFORE SYSTEM "." - ${SDL2_INCLUDE_DIR} - ${Boost_INCLUDE_DIR} ${MyGUI_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} @@ -494,7 +500,7 @@ include_directories( ${ICU_INCLUDE_DIRS} ) -link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS} ${COLLADA_DOM_LIBRARY_DIRS}) +link_directories(${COLLADA_DOM_LIBRARY_DIRS}) if(MYGUI_STATIC) add_definitions(-DMYGUI_STATIC) @@ -536,14 +542,19 @@ pack_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}" "openmw.appdata.xml") -if (NOT APPLE) +if (APPLE) + configure_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg + "${OpenMW_BINARY_DIR}/openmw.cfg") +elseif (WIN32) + configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local + "${OpenMW_BINARY_DIR}" "openmw.cfg") + configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local + "${OpenMW_BINARY_DIR}" "openmw.cfg.install") +else () configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local "${OpenMW_BINARY_DIR}" "openmw.cfg") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg "${OpenMW_BINARY_DIR}" "openmw.cfg.install") -else () - configure_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg - "${OpenMW_BINARY_DIR}/openmw.cfg") endif () pack_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg @@ -582,7 +593,6 @@ endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) set(OPENMW_CXX_FLAGS "-Wall -Wextra -Wundef -Wextra-semi -Wno-unused-parameter -pedantic -Wno-long-long -Wnon-virtual-dtor -Wunused ${OPENMW_CXX_FLAGS}") - add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) if (APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") @@ -617,7 +627,7 @@ endif() add_subdirectory (components) # Apps and tools -if (BUILD_OPENMW) +if (BUILD_OPENMW OR BUILD_OPENMW_TESTS) add_subdirectory( apps/openmw ) endif() @@ -651,11 +661,10 @@ endif() if (BUILD_NIFTEST) add_subdirectory(apps/niftest) -endif(BUILD_NIFTEST) +endif() -# UnitTests -if (BUILD_UNITTESTS) - add_subdirectory( apps/openmw_test_suite ) +if (BUILD_COMPONENTS_TESTS) + add_subdirectory(apps/components_tests) endif() if (BUILD_BENCHMARKS) @@ -667,13 +676,17 @@ if (BUILD_NAVMESHTOOL) endif() if (BUILD_BULLETOBJECTTOOL) - add_subdirectory( apps/bulletobjecttool ) + add_subdirectory(apps/bulletobjecttool) endif() if (BUILD_OPENCS_TESTS) add_subdirectory(apps/opencs_tests) endif() +if (BUILD_OPENMW_TESTS) + add_subdirectory(apps/openmw_tests) +endif() + if (WIN32) if (MSVC) foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) @@ -685,9 +698,8 @@ if (WIN32) if (USE_DEBUG_CONSOLE AND BUILD_OPENMW) set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") - set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS $<$:_CONSOLE>) elseif (BUILD_OPENMW) - # Turn off debug console, debug output will be written to visual studio output instead + # Turn off implicit console, you won't see stdout unless launching OpenMW from a command line shell or look at openmw.log set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:WINDOWS") set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS") endif() @@ -710,67 +722,74 @@ if (WIN32) ) foreach(d ${WARNINGS_DISABLE}) - set(WARNINGS "${WARNINGS} /wd${d}") + list(APPEND WARNINGS "/wd${d}") endforeach(d) if(OPENMW_MSVC_WERROR) - set(WARNINGS "${WARNINGS} /WX") + list(APPEND WARNINGS "/WX") endif() - set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS}") - set_target_properties(osg-ffmpeg-videoplayer PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(components PRIVATE ${WARNINGS}) + target_compile_options(osg-ffmpeg-videoplayer PRIVATE ${WARNINGS}) if (MSVC_VERSION GREATER_EQUAL 1915 AND MSVC_VERSION LESS 1920) target_compile_definitions(components INTERFACE _ENABLE_EXTENDED_ALIGNED_STORAGE) endif() if (BUILD_BSATOOL) - set_target_properties(bsatool PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(bsatool PRIVATE ${WARNINGS}) endif() if (BUILD_ESMTOOL) - set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(esmtool PRIVATE ${WARNINGS}) endif() if (BUILD_ESSIMPORTER) - set_target_properties(openmw-essimporter PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-essimporter PRIVATE ${WARNINGS}) endif() if (BUILD_LAUNCHER) - set_target_properties(openmw-launcher PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-launcher PRIVATE ${WARNINGS}) endif() if (BUILD_MWINIIMPORTER) - set_target_properties(openmw-iniimporter PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-iniimporter PRIVATE ${WARNINGS}) endif() if (BUILD_OPENCS) - set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-cs PRIVATE ${WARNINGS}) endif() if (BUILD_OPENMW) - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw PRIVATE ${WARNINGS}) endif() if (BUILD_WIZARD) - set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-wizard PRIVATE ${WARNINGS}) endif() - if (BUILD_UNITTESTS) - set_target_properties(openmw_test_suite PROPERTIES COMPILE_FLAGS "${WARNINGS}") + if (BUILD_COMPONENTS_TESTS) + target_compile_options(components-tests PRIVATE ${WARNINGS}) endif() if (BUILD_BENCHMARKS) - set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ${WARNINGS}) endif() if (BUILD_NAVMESHTOOL) - set_target_properties(openmw-navmeshtool PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-navmeshtool PRIVATE ${WARNINGS}) endif() if (BUILD_BULLETOBJECTTOOL) - set(WARNINGS "${WARNINGS} ${MT_BUILD}") - set_target_properties(openmw-bulletobjecttool PROPERTIES COMPILE_FLAGS "${WARNINGS}") + target_compile_options(openmw-bulletobjecttool PRIVATE ${WARNINGS} ${MT_BUILD}) + endif() + + if (BUILD_OPENCS_TESTS) + target_compile_options(openmw-cs-tests PRIVATE ${WARNINGS}) + endif() + + if (BUILD_OPENMW_TESTS) + target_compile_options(openmw-tests PRIVATE ${WARNINGS}) endif() endif(MSVC) @@ -802,6 +821,18 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) get_filename_component(QT_QMACSTYLE_PLUGIN_NAME "${QT_QMACSTYLE_PLUGIN_PATH}" NAME) configure_file("${QT_QMACSTYLE_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}" COPYONLY) + get_property(QT_QSVG_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgPlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QSVG_PLUGIN_DIR "${QT_QSVG_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QSVG_PLUGIN_GROUP "${QT_QSVG_PLUGIN_DIR}" NAME) + get_filename_component(QT_QSVG_PLUGIN_NAME "${QT_QSVG_PLUGIN_PATH}" NAME) + configure_file("${QT_QSVG_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QSVG_PLUGIN_GROUP}/${QT_QSVG_PLUGIN_NAME}" COPYONLY) + + get_property(QT_QSVG_ICON_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgIconPlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QSVG_ICON_PLUGIN_DIR "${QT_QSVG_ICON_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QSVG_ICON_PLUGIN_GROUP "${QT_QSVG_ICON_PLUGIN_DIR}" NAME) + get_filename_component(QT_QSVG_ICON_PLUGIN_NAME "${QT_QSVG_ICON_PLUGIN_PATH}" NAME) + configure_file("${QT_QSVG_ICON_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QSVG_ICON_PLUGIN_GROUP}/${QT_QSVG_ICON_PLUGIN_NAME}" COPYONLY) + configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${APP_BUNDLE_DIR}/Contents/Resources/qt.conf" COPYONLY) if (BUILD_OPENCS) @@ -809,6 +840,8 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) set(OPENCS_BUNDLE_NAME "${OPENCS_BUNDLE_NAME_TMP}.app") configure_file("${QT_COCOA_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) configure_file("${QT_QMACSTYLE_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}" COPYONLY) + configure_file("${QT_QSVG_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QSVG_PLUGIN_GROUP}/${QT_QSVG_PLUGIN_NAME}" COPYONLY) + configure_file("${QT_QSVG_ICON_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QSVG_ICON_PLUGIN_GROUP}/${QT_QSVG_ICON_PLUGIN_NAME}" COPYONLY) configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${OPENCS_BUNDLE_NAME}/Contents/Resources/qt.conf" COPYONLY) endif () @@ -877,6 +910,11 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) install_plugins_for_bundle("${APP_BUNDLE_NAME}" PLUGINS) install_plugins_for_bundle("${OPENCS_BUNDLE_NAME}" OPENCS_PLUGINS) + INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/AUTHORS.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}") set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}") set(OPENCS_PLUGINS ${OPENCS_PLUGINS} "${INSTALLED_OPENCS_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}") @@ -928,6 +966,7 @@ elseif(NOT APPLE) INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "." RENAME "LICENSE.txt") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/AUTHORS.md" DESTINATION "." RENAME "AUTHORS.txt") INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION ".") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".") @@ -1041,7 +1080,6 @@ elseif(NOT APPLE) # Install global configuration files INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw") - INSTALL(FILES "${INSTALL_SOURCE}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") IF(BUILD_OPENCS) @@ -1053,12 +1091,17 @@ elseif(NOT APPLE) endif(WIN32) endif(OPENMW_OSX_DEPLOYMENT AND APPLE) -# Doxygen Target -- simply run 'make doc' or 'make doc_pages' -# output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" -# output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined -# or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise -find_package(Doxygen) -if (DOXYGEN_FOUND) +# what is necessary to build documentation +if ( BUILD_DOCS ) + # Builds the documentation. + FIND_PACKAGE( Sphinx REQUIRED ) + FIND_PACKAGE( Doxygen REQUIRED ) + + # Doxygen Target -- simply run 'make doc' or 'make doc_pages' + # output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" + # output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined + # or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise + # determine output directory for doc_pages if (NOT DEFINED DOXYGEN_PAGES_OUTPUT_DIR) set(DOXYGEN_PAGES_OUTPUT_DIR "${OpenMW_BINARY_DIR}/docs/Pages") @@ -1075,3 +1118,78 @@ if (DOXYGEN_FOUND) WORKING_DIRECTORY ${OpenMW_BINARY_DIR} COMMENT "Generating documentation for the github-pages at ${DOXYGEN_PAGES_OUTPUT_DIR}" VERBATIM) endif () + +# Qt localization +if (USE_QT) + file(GLOB LAUNCHER_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/launcher_*.ts) + file(GLOB WIZARD_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/wizard_*.ts) + file(GLOB COMPONENTS_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/components_*.ts) + get_target_property(QT_LUPDATE_EXECUTABLE Qt::lupdate IMPORTED_LOCATION) + add_custom_target(translations + COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/components/contentselector ${CMAKE_SOURCE_DIR}/components/process -ts ${COMPONENTS_TS_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/components + VERBATIM + COMMAND_EXPAND_LISTS + + COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/apps/wizard -ts ${WIZARD_TS_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/apps/wizard + VERBATIM + COMMAND_EXPAND_LISTS + + COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/apps/launcher -ts ${LAUNCHER_TS_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/apps/launcher + VERBATIM + COMMAND_EXPAND_LISTS) + + if (BUILD_LAUNCHER OR BUILD_WIZARD) + if (APPLE) + set(QT_OPENMW_TRANSLATIONS_PATH "${APP_BUNDLE_DIR}/Contents/Resources/resources/translations") + else () + get_generator_is_multi_config(multi_config) + if (multi_config) + set(QT_OPENMW_TRANSLATIONS_PATH "${OpenMW_BINARY_DIR}/$/resources/translations") + else () + set(QT_OPENMW_TRANSLATIONS_PATH "${OpenMW_BINARY_DIR}/resources/translations") + endif () + endif () + + file(GLOB TS_FILES ${COMPONENTS_TS_FILES}) + + if (BUILD_LAUNCHER) + set(TS_FILES ${TS_FILES} ${LAUNCHER_TS_FILES}) + endif () + + if (BUILD_WIZARD) + set(TS_FILES ${TS_FILES} ${WIZARD_TS_FILES}) + endif () + + qt_add_translation(QM_FILES ${TS_FILES} OPTIONS -silent) + + if (DEPLOY_QT_TRANSLATIONS) + # Once we set a Qt 6.2.0 as a minimum required version, we may use "qtpaths --qt-query" instead. + get_target_property(QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION) + execute_process(COMMAND "${QT_QMAKE_EXECUTABLE}" -query QT_INSTALL_TRANSLATIONS + OUTPUT_VARIABLE QT_TRANSLATIONS_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) + + foreach(QM_FILE ${QM_FILES}) + get_filename_component(QM_BASENAME ${QM_FILE} NAME) + string(REGEX REPLACE "[^_]+_(.*)\\.qm" "\\1" LANG_NAME ${QM_BASENAME}) + if (EXISTS "${QT_TRANSLATIONS_DIR}/qtbase_${LANG_NAME}.qm") + set(QM_FILES ${QM_FILES} "${QT_TRANSLATIONS_DIR}/qtbase_${LANG_NAME}.qm") + elseif (EXISTS "${QT_TRANSLATIONS_DIR}/qt_${LANG_NAME}.qm") + set(QM_FILES ${QM_FILES} "${QT_TRANSLATIONS_DIR}/qt_${LANG_NAME}.qm") + else () + message(FATAL_ERROR "Qt translations for '${LANG_NAME}' locale are not found in the '${QT_TRANSLATIONS_DIR}' folder.") + endif () + endforeach(QM_FILE) + + list(REMOVE_DUPLICATES QM_FILES) + endif () + + add_custom_target(qm-files + COMMAND ${CMAKE_COMMAND} -E make_directory ${QT_OPENMW_TRANSLATIONS_PATH} + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${QM_FILES} ${QT_OPENMW_TRANSLATIONS_PATH} + DEPENDS ${QM_FILES} + COMMENT "Copy *.qm files to resources folder") + endif () +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4cdb164a3b..8aeb455543 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,37 +8,37 @@ https://gitlab.com/OpenMW/openmw/issues Currently, we are focused on completing the MW game experience and general polishing. Features out of this scope may be approved in some cases, but you should probably start a discussion first. Note: -* Tasks set to 'openmw-future' are usually out of the current scope of the project and can't be started yet. -* Bugs that are not 'Confirmed' should be confirmed first. +* Issues that have the 'Future' label are usually out of the current scope of the project. Corresponding submissions are unlikely to be merged or even properly reviewed. +* Newly reported bugs should be attempted to be reproduced on the latest code and on the latest available stable release. Both can be found [here](https://openmw.org/downloads/). * Often, it's best to start a discussion about possible solutions before you jump into coding, especially for larger features. -Aside from coding, you can also help by triaging the issues list. Check for bugs that are 'Unconfirmed' and try to confirm them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug reporting guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so! +Aside from coding, you can also help by triaging the issues list. Check for unconfirmed bugs and try to reproduce them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug Reporting Guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so! There are various [Tools](https://wiki.openmw.org/index.php?title=Tools) to facilitate testing/development. -Pull Request Guidelines +Merge request guidelines ======================= -To facilitate the review process, your pull request description should include the following, if applicable: +To facilitate the review process, your merge request description should include the following, if applicable: -* A link back to the bug report or forum discussion that prompted the change. Note: when linking bugs, use the syntax ```[Bug #xyz](https://gitlab.com/OpenMW/openmw/issues/#xyz)``` to create a clickable link. Writing only 'Bug #xyz' will unfortunately create a link to the Github pull request with that number instead. +* A link back to the bug report or discussion that prompted the change. * Summary of the changes made. * Reasoning / motivation behind the change. * What testing you have carried out to verify the change. Furthermore, we advise to: -* Avoid stuffing unrelated commits into one pull request. As a rule of thumb, each feature and each bugfix should go into a separate PR, unless they are closely related or dependent upon each other. Small pull requests are easier to review, and are less likely to require further changes before we can merge them. A "mega" pull request with lots of unrelated commits in it is likely to get held up in review for a long time. -* Feel free to submit incomplete pull requests. Even if the work can not be merged yet, pull requests are a great place to collect early feedback. Just make sure to mark it as *[Incomplete]* or *[Do not merge yet]* in the title. +* Avoid stuffing unrelated commits into one merge request. As a rule of thumb, each feature and each bugfix should go into a separate MR, unless they are closely related or dependent upon each other. Small merge requests are easier to review and are less likely to require further changes before we can merge them. A "mega" merge request with lots of unrelated commits in it is likely to get held up in review for a long time. +* Feel free to submit incomplete merge requests. Even if the work cannot be merged yet, merge requests are a great place to collect early feedback. Just make sure to mark it as [draft](https://docs.gitlab.com/ee/user/project/merge_requests/drafts/). * If you plan on contributing often, please read the [Developer Reference](https://wiki.openmw.org/index.php?title=Developer_Reference) on our wiki, especially the [Policies and Standards](https://wiki.openmw.org/index.php?title=Policies_and_Standards). * Make sure each of your changes has a clear objective. Unnecessary changes may lead to merge conflicts, clutter the commit history and slow down review. Code formatting 'fixes' should be avoided, unless you were already changing that particular line anyway. -* Reference the bug / feature ticket(s) in your commit message (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your commit message includes 'Fixes #123', that bug/feature will automatically be set to 'Closed' when your commit is merged. -* When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running PRs. +* Reference the bug / feature ticket(s) in your commit message or merge request description (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your merge request's description includes 'Fixes #123', that issue will automatically be closed when your commit is merged. +* When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running MRs. Guidelines for original engine "fixes" ================================= -From time to time you may be tempted to "fix" what you think was a "bug" in the original game engine. +From time to time, you may be tempted to "fix" what you think was a "bug" in the original game engine. Unfortunately, the definition of what is a "bug" is not so clear. Consider that your "bug" is actually a feature unless proven otherwise: @@ -52,56 +52,62 @@ OpenMW, in its default configuration, is meant to be a faithful reimplementation That said, we may sometimes evaluate such issues on an individual basis. Common exceptions to the above would be: * Issues so glaring that they would severely limit the capabilities of the engine in the future (for example, the scripting engine not being allowed to access objects in remote cells). -* Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug were being tired made it easier to repair items, instead of harder). +* Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug where being tired made it easier to repair items, instead of harder). * Bugs that were fixed in an official patch for Morrowind. Feature additions policy ===================== -We get it, you have waited so long for feature XYZ to be available in Morrowind and now that OpenMW is here you can not wait to implement your ingenious idea and share it with the world. +We get it: you have waited so long for feature XYZ to be available in Morrowind, and now that OpenMW is here, you cannot wait to implement your ingenious idea and share it with the world. Unfortunately, since maintaining features comes at a cost and our resources are limited, we have to be a little selective in what features we allow into the main repository. Generally: * Features should be as generic and non-redundant as possible. * Any feature that is also possible with modding should be done as a mod instead. -* In the future, OpenMW plans to expand the scope of what is possible with modding, e.g. by moving certain game logic into editable scripts. -* Currently, modders can edit OpenMW's GUI skins and layout XML files, although there are still a few missing hooks (e.g. scripting support) in order to make this into a powerful way of modding. -* If a feature introduces new game UI strings, that reduces its chance of being accepted because we do not currently have any way of localizing these to the user's Morrowind installation language. +* Through moving certain game logic into built-in scripting, OpenMW will expand the scope of what is possible with modding. +* Modders can edit OpenMW's GUI skins and layout XML files as well as create new widgets through the Lua API, but it is expected that existing C++ widgets will also be recreated through built-in scripting. +* If a feature introduces new game UI strings, you will need to become acquainted with OpenMW's YAML localisation system and expose them. Read about it [here](https://openmw.readthedocs.io/en/latest/reference/modding/localisation.html). If you are in doubt of your feature being within our scope, it is probably best to start a forum discussion first. See the [settings documentation](https://openmw.readthedocs.io/en/stable/reference/modding/settings/index.html) and [Features list](https://wiki.openmw.org/index.php?title=Features) for some examples of features that were deemed acceptable. -Reviewing pull requests +Reviewing merge requests ======================= -We welcome any help in reviewing open PRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/). +We welcome any help in reviewing open MRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/). -This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the PR is deemed OK. Anyone can help with the Functionality Review while most parts of the Code Review require you to have programming experience. +This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the MR is deemed OK. Anyone can help with the **functionality review** while most parts of the **code review** require you to have programming experience. -In addition to the checklist below, make sure to check that the Pull Request Guidelines (first half of this document) were followed. +In addition to the checklist below, make sure to check that the **merge request guidelines** (first half of this document) were followed. -First review +Functionality review ============ 1. Ask for missing information or clarifications. Compare against the project's design goals and roadmap. -2. Check if the automated tests are passing. If they are not, make the PR author aware of the issue and potentially link to the error line on Travis CI or Appveyor. If the error appears unrelated to the PR and/or the master branch is failing with the same error, our CI has broken and needs to be fixed independently of any open PRs. Raise this issue on the forums, bug tracker or with the relevant maintainer. The PR can be merged in this case as long as you've built it yourself to make sure it does build. -3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a PR that no one has tested so far, post a comment letting us know. -4. On long running PRs, request the author to update its description with the current state or a checklist of things left to do. +2. Check if the automated tests are passing. If they are not, make the MR author aware of the issue and potentially quote the error line. If the error appears unrelated to the MR and/or the master branch is failing with the same error, our CI might be broken and needs to be fixed independently of any open MRs. Raise this issue on one of the following resources: + * Our [forums](https://forum.openmw.org/) + * [Discord](https://discord.com/servers/openmw-260439894298460160) + * [IRC](https://web.libera.chat/#openmw) + * [Issue tracker](https://gitlab.com/OpenMW/openmw/-/issues) -Code Review +3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a MR that no one has tested so far, post a comment letting us know. +4. On long-running MRs, request the author to update its description with the current state or a checklist of things left to do. + +Code review =========== 1. Carefully review each line for issues the author may not have thought of, paying special attention to 'special' cases. Often, people build their code with a particular mindset and forget about other configurations or unexpected interactions. 2. If any changes are workarounds for an issue in an upstream library, make sure the issue was reported upstream so we can eventually drop the workaround when the issue is fixed and the new version of that library is a build dependency. -3. Make sure PRs do not turn into arguments about hardly related issues. If the PR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the PR to adhere to the established way, rather than leaving the PR hanging on a dispute. +3. Make sure MRs do not turn into arguments about hardly related issues. If the MR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the MR to adhere to the established way, rather than leaving the MR hanging on a dispute. 4. Check if the code matches our style guidelines. 5. Check to make sure the commit history is clean. Squashing should be considered if the review process has made the commit history particularly long. Commits that don't build should be avoided because they are a nuisance for ```git bisect```. Merging ======= -To be able to merge PRs, commit privileges are required. If you do not have the privileges, just ping someone that does have them with a short comment like "Looks good to me @user". +To be able to merge MRs, commit privileges are required. If you do not have the privileges, just ping someone that does have them with a short comment like "Looks good to me @user". -The person to merge the PR may either use github's Merge button or if using the command line, use the ```--no-ff``` flag (so a merge commit is created, just like with Github's merge button) and include the pull request number in the commit description. +In general case, you should not merge MRs prematurely even if you are sure they just work or if they receive a senior member's approval. +The rule of thumb is to give at least 24 hours to a couple days of a window for reviews to come through. For more technically involved MRs, 24 hours might not be enough. Dealing with regressions ======================== diff --git a/README.md b/README.md index 95ca19685d..bca5851c7b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Font Licenses: Current Status -------------- -The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/issues?label_name%5B%5D=1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces. +The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/-/issues/?milestone_title=openmw-1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces. Pre-existing modifications created for the original Morrowind engine can be hit-and-miss. The OpenMW script compiler performs more thorough error-checking than Morrowind does, meaning that a mod created for Morrowind may not necessarily run in OpenMW. Some mods also rely on quirky behaviour or engine bugs in order to work. We are considering such compatibility issues on a case-by-case basis - in some cases adding a workaround to OpenMW may be feasible, in other cases fixing the mod will be the only option. If you know of any mods that work or don't work, feel free to add them to the [Mod status](https://wiki.openmw.org/index.php?title=Mod_status) wiki page. @@ -74,10 +74,6 @@ Command line options 1 - show warning but consider script as correctly compiled anyway 2 - treat warnings as errors - --script-blacklist arg ignore the specified script (if the use - of the blacklist is enabled) - --script-blacklist-use [=arg(=1)] (=1) - enable script blacklisting --load-savegame arg load a save game file on game startup (specify an absolute filename or a filename relative to the current diff --git a/apps/benchmarks/detournavigator/CMakeLists.txt b/apps/benchmarks/detournavigator/CMakeLists.txt index 2b3a6abe51..ffe7818a5a 100644 --- a/apps/benchmarks/detournavigator/CMakeLists.txt +++ b/apps/benchmarks/detournavigator/CMakeLists.txt @@ -5,7 +5,7 @@ if (UNIX AND NOT APPLE) target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ) endif() diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp index 3be1c8762a..26873d9a03 100644 --- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace @@ -24,29 +25,25 @@ namespace PreparedNavMeshData mValue; }; - template - osg::Vec2i generateVec2i(int max, Random& random) + osg::Vec2i generateVec2i(int max, auto& random) { std::uniform_int_distribution distribution(0, max); return osg::Vec2i(distribution(random), distribution(random)); } - template - osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) + osg::Vec3f generateAgentHalfExtents(float min, float max, auto& random) { std::uniform_int_distribution distribution(min, max); return osg::Vec3f(distribution(random), distribution(random), distribution(random)); } - template - void generateVertices(OutputIterator out, std::size_t number, Random& random) + void generateVertices(std::output_iterator auto out, std::size_t number, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); }); } - template - void generateIndices(OutputIterator out, int max, std::size_t number, Random& random) + void generateIndices(std::output_iterator auto out, int max, std::size_t number, auto& random) { std::uniform_int_distribution distribution(0, max); std::generate_n(out, number - number % 3, [&] { return distribution(random); }); @@ -70,21 +67,18 @@ namespace return AreaType_null; } - template - AreaType generateAreaType(Random& random) + AreaType generateAreaType(auto& random) { std::uniform_int_distribution distribution(0, 4); return toAreaType(distribution(random)); } - template - void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) + void generateAreaTypes(std::output_iterator auto out, std::size_t triangles, auto& random) { std::generate_n(out, triangles, [&] { return generateAreaType(random); }); } - template - void generateWater(OutputIterator out, std::size_t count, Random& random) + void generateWater(std::output_iterator auto out, std::size_t count, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { @@ -92,8 +86,7 @@ namespace }); } - template - Mesh generateMesh(std::size_t triangles, Random& random) + Mesh generateMesh(std::size_t triangles, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::vector vertices; @@ -109,8 +102,7 @@ namespace return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); } - template - Heightfield generateHeightfield(Random& random) + Heightfield generateHeightfield(auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); Heightfield result; @@ -127,8 +119,7 @@ namespace return result; } - template - FlatHeightfield generateFlatHeightfield(Random& random) + FlatHeightfield generateFlatHeightfield(auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); FlatHeightfield result; @@ -138,8 +129,7 @@ namespace return result; } - template - Key generateKey(std::size_t triangles, Random& random) + Key generateKey(std::size_t triangles, auto& random) { const CollisionShapeType agentShapeType = CollisionShapeType::Aabb; const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); @@ -158,14 +148,12 @@ namespace constexpr std::size_t trianglesPerTile = 239; - template - void generateKeys(OutputIterator out, std::size_t count, Random& random) + void generateKeys(std::output_iterator auto out, std::size_t count, auto& random) { std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); } - template - void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) + void fillCache(std::output_iterator auto out, auto& random, NavMeshTilesCache& cache) { std::size_t size = cache.getStats().mNavMeshCacheSize; @@ -194,7 +182,7 @@ namespace for (auto _ : state) { const auto& key = keys[n++ % keys.size()]; - const auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh); + auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh); benchmark::DoNotOptimize(result); } } @@ -253,7 +241,7 @@ namespace while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; - const auto result = cache.set( + auto result = cache.set( key.mAgentBounds, key.mTilePosition, key.mRecastMesh, std::make_unique()); benchmark::DoNotOptimize(result); } diff --git a/apps/benchmarks/esm/CMakeLists.txt b/apps/benchmarks/esm/CMakeLists.txt index 74870ceda1..9b5afd649d 100644 --- a/apps/benchmarks/esm/CMakeLists.txt +++ b/apps/benchmarks/esm/CMakeLists.txt @@ -5,7 +5,7 @@ if (UNIX AND NOT APPLE) target_link_libraries(openmw_esm_refid_benchmark ${CMAKE_THREAD_LIBS_INIT}) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw_esm_refid_benchmark PRIVATE ) endif() diff --git a/apps/benchmarks/settings/CMakeLists.txt b/apps/benchmarks/settings/CMakeLists.txt index ccdd51eeac..51e2d2b0fd 100644 --- a/apps/benchmarks/settings/CMakeLists.txt +++ b/apps/benchmarks/settings/CMakeLists.txt @@ -8,7 +8,7 @@ if (UNIX AND NOT APPLE) target_link_libraries(openmw_settings_access_benchmark ${CMAKE_THREAD_LIBS_INIT}) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw_settings_access_benchmark PRIVATE ) endif() diff --git a/apps/benchmarks/settings/access.cpp b/apps/benchmarks/settings/access.cpp index aecac2dac8..7660d0d55e 100644 --- a/apps/benchmarks/settings/access.cpp +++ b/apps/benchmarks/settings/access.cpp @@ -38,7 +38,7 @@ namespace { for (auto _ : state) { - static const float v = Settings::Manager::getFloat("sky blending start", "Fog"); + static float v = Settings::Manager::getFloat("sky blending start", "Fog"); benchmark::DoNotOptimize(v); } } @@ -47,8 +47,8 @@ namespace { for (auto _ : state) { - static const float v1 = Settings::Manager::getFloat("near clip", "Camera"); - static const bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); + static float v1 = Settings::Manager::getFloat("near clip", "Camera"); + static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); benchmark::DoNotOptimize(v1); benchmark::DoNotOptimize(v2); } @@ -58,9 +58,9 @@ namespace { for (auto _ : state) { - static const float v1 = Settings::Manager::getFloat("near clip", "Camera"); - static const bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); - static const int v3 = Settings::Manager::getInt("reflection detail", "Water"); + static float v1 = Settings::Manager::getFloat("near clip", "Camera"); + static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); + static int v3 = Settings::Manager::getInt("reflection detail", "Water"); benchmark::DoNotOptimize(v1); benchmark::DoNotOptimize(v2); benchmark::DoNotOptimize(v3); @@ -71,7 +71,8 @@ namespace { for (auto _ : state) { - benchmark::DoNotOptimize(Settings::fog().mSkyBlendingStart.get()); + float v = Settings::fog().mSkyBlendingStart.get(); + benchmark::DoNotOptimize(v); } } @@ -79,8 +80,10 @@ namespace { for (auto _ : state) { - benchmark::DoNotOptimize(Settings::postProcessing().mTransparentPostpass.get()); - benchmark::DoNotOptimize(Settings::camera().mNearClip.get()); + bool v1 = Settings::postProcessing().mTransparentPostpass.get(); + float v2 = Settings::camera().mNearClip.get(); + benchmark::DoNotOptimize(v1); + benchmark::DoNotOptimize(v2); } } @@ -88,9 +91,12 @@ namespace { for (auto _ : state) { - benchmark::DoNotOptimize(Settings::postProcessing().mTransparentPostpass.get()); - benchmark::DoNotOptimize(Settings::camera().mNearClip.get()); - benchmark::DoNotOptimize(Settings::water().mReflectionDetail.get()); + bool v1 = Settings::postProcessing().mTransparentPostpass.get(); + float v2 = Settings::camera().mNearClip.get(); + int v3 = Settings::water().mReflectionDetail.get(); + benchmark::DoNotOptimize(v1); + benchmark::DoNotOptimize(v2); + benchmark::DoNotOptimize(v3); } } diff --git a/apps/bsatool/CMakeLists.txt b/apps/bsatool/CMakeLists.txt index a567499ac6..b2ad8f16b2 100644 --- a/apps/bsatool/CMakeLists.txt +++ b/apps/bsatool/CMakeLists.txt @@ -9,7 +9,7 @@ openmw_add_executable(bsatool ) target_link_libraries(bsatool - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options components ) @@ -18,7 +18,7 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(bsatool gcov) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(bsatool PRIVATE diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index e2029f3245..171e5606c4 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -194,7 +194,8 @@ int extract(std::unique_ptr& bsa, Arguments& info) // Get a stream for the file to extract for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it) { - if (Misc::StringUtils::ciEqual(Misc::StringUtils::stringToU8String(it->name()), archivePath)) + auto streamPath = Misc::StringUtils::stringToU8String(it->name()); + if (Misc::StringUtils::ciEqual(streamPath, archivePath) || Misc::StringUtils::ciEqual(streamPath, extractPath)) { stream = bsa->getFile(&*it); break; @@ -320,21 +321,27 @@ int main(int argc, char** argv) // Open file + // TODO: add a version argument for this mode after compressed BSA writing is a thing + if (info.mode == "create") + return call(info); + Bsa::BsaVersion bsaVersion = Bsa::BSAFile::detectVersion(info.filename); switch (bsaVersion) { - case Bsa::BSAVER_COMPRESSED: - return call(info); - case Bsa::BSAVER_BA2_GNRL: - return call(info); - case Bsa::BSAVER_BA2_DX10: - return call(info); - case Bsa::BSAVER_UNCOMPRESSED: + case Bsa::BsaVersion::Unknown: + break; + case Bsa::BsaVersion::Uncompressed: return call(info); - default: - throw std::runtime_error("Unrecognised BSA archive"); + case Bsa::BsaVersion::Compressed: + return call(info); + case Bsa::BsaVersion::BA2GNRL: + return call(info); + case Bsa::BsaVersion::BA2DX10: + return call(info); } + + throw std::runtime_error("Unrecognised BSA archive"); } catch (std::exception& e) { diff --git a/apps/bulletobjecttool/CMakeLists.txt b/apps/bulletobjecttool/CMakeLists.txt index 6e6e1cdbb3..c29915b139 100644 --- a/apps/bulletobjecttool/CMakeLists.txt +++ b/apps/bulletobjecttool/CMakeLists.txt @@ -6,7 +6,7 @@ source_group(apps\\bulletobjecttool FILES ${BULLETMESHTOOL}) openmw_add_executable(openmw-bulletobjecttool ${BULLETMESHTOOL}) target_link_libraries(openmw-bulletobjecttool - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options components ) @@ -19,7 +19,7 @@ if (WIN32) install(TARGETS openmw-bulletobjecttool RUNTIME DESTINATION ".") endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-bulletobjecttool PRIVATE diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index 2610061af6..190eb3364d 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -76,10 +77,6 @@ namespace bpo::value()->default_value(StringsVector(), "fallback-archive")->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)"); - addOption("resources", - bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), - "set resources directory"); - addOption("content", bpo::value()->default_value(StringsVector(), "")->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts"); @@ -124,14 +121,14 @@ namespace if (variables.find("help") != variables.end()) { - getRawStdout() << desc << std::endl; + Debug::getRawStdout() << desc << std::endl; return 0; } Files::ConfigurationManager config; config.readConfiguration(variables, desc); - setupLogging(config.getLogPath(), applicationName); + Debug::setupLogging(config.getLogPath(), applicationName); const std::string encoding(variables["encoding"].as()); Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); @@ -145,13 +142,14 @@ namespace config.filterOutNonExistingPaths(dataDirs); - const auto resDir = variables["resources"].as(); - const auto v = Version::getOpenmwVersion(resDir); - Log(Debug::Info) << v.describe(); + const auto& resDir = variables["resources"].as(); + Log(Debug::Info) << Version::getOpenmwVersionDescription(); dataDirs.insert(dataDirs.begin(), resDir / "vfs"); - const auto fileCollections = Files::Collections(dataDirs); - const auto archives = variables["fallback-archive"].as(); - const auto contentFiles = variables["content"].as(); + const Files::Collections fileCollections(dataDirs); + const auto& archives = variables["fallback-archive"].as(); + StringsVector contentFiles{ "builtin.omwscripts" }; + const auto& configContentFiles = variables["content"].as(); + contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end()); Fallback::Map::init(variables["fallback"].as().mMap); @@ -173,10 +171,12 @@ namespace const EsmLoader::EsmData esmData = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder); - Resource::ImageManager imageManager(&vfs); - Resource::NifFileManager nifFileManager(&vfs); - Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager); - Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager); + constexpr double expiryDelay = 0; + Resource::ImageManager imageManager(&vfs, expiryDelay); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); + Resource::BgsmFileManager bgsmFileManager(&vfs, expiryDelay); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); + Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); Resource::forEachBulletObject( readers, vfs, bulletShapeManager, esmData, [](const ESM::Cell& cell, const Resource::BulletObject& object) { @@ -202,5 +202,5 @@ namespace int main(int argc, char* argv[]) { - return wrapApplication(runBulletObjectTool, argc, argv, applicationName); + return Debug::wrapApplication(runBulletObjectTool, argc, argv, applicationName); } diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/components_tests/CMakeLists.txt similarity index 75% rename from apps/openmw_test_suite/CMakeLists.txt rename to apps/components_tests/CMakeLists.txt index 4f93319c96..22bb542538 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/components_tests/CMakeLists.txt @@ -2,19 +2,7 @@ include_directories(SYSTEM ${GTEST_INCLUDE_DIRS}) include_directories(SYSTEM ${GMOCK_INCLUDE_DIRS}) file(GLOB UNITTEST_SRC_FILES - testing_util.hpp - - ../openmw/mwworld/store.cpp - ../openmw/mwworld/esmstore.cpp - ../openmw/mwworld/timestamp.cpp - - mwworld/test_store.cpp - mwworld/testduration.cpp - mwworld/testtimestamp.cpp - - mwdialogue/test_keywordsearch.cpp - - mwscript/test_scripts.cpp + main.cpp esm/test_fixed_string.cpp esm/variant.cpp @@ -28,14 +16,17 @@ file(GLOB UNITTEST_SRC_FILES lua/test_l10n.cpp lua/test_storage.cpp lua/test_async.cpp + lua/test_inputactions.cpp + lua/test_yaml.cpp lua/test_ui_content.cpp - misc/test_stringops.cpp + misc/compression.cpp + misc/progressreporter.cpp misc/test_endianness.cpp misc/test_resourcehelpers.cpp - misc/progressreporter.cpp - misc/compression.cpp + misc/test_stringops.cpp + misc/testmathutil.cpp nifloader/testbulletnifloader.cpp @@ -64,9 +55,6 @@ file(GLOB UNITTEST_SRC_FILES shader/parselinks.cpp shader/shadermanager.cpp - ../openmw/options.cpp - openmw/options.cpp - sqlite3/db.cpp sqlite3/request.cpp sqlite3/statement.cpp @@ -90,25 +78,37 @@ file(GLOB UNITTEST_SRC_FILES esm3/testsaveload.cpp esm3/testesmwriter.cpp esm3/testinfoorder.cpp + esm3/testcstringids.cpp nifosg/testnifloader.cpp esmterrain/testgridsampling.cpp + + resource/testobjectcache.cpp + + vfs/testpathutil.cpp + + sceneutil/osgacontroller.cpp ) -source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) +source_group(apps\\components-tests FILES ${UNITTEST_SRC_FILES}) -openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) +openmw_add_executable(components-tests ${UNITTEST_SRC_FILES}) + +target_link_libraries(components-tests + GTest::GTest + GMock::GMock + components +) -target_link_libraries(openmw_test_suite GTest::GTest GMock::GMock components) # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) - target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT}) + target_link_libraries(components-tests ${CMAKE_THREAD_LIBS_INIT}) endif() if (BUILD_WITH_CODE_COVERAGE) - target_compile_options(openmw_test_suite PRIVATE --coverage) - target_link_libraries(openmw_test_suite gcov) + target_compile_options(components-tests PRIVATE --coverage) + target_link_libraries(components-tests gcov) endif() file(DOWNLOAD @@ -117,12 +117,12 @@ file(DOWNLOAD EXPECTED_HASH SHA512=6e38642bcf013c5f496a9cb0bf3ec7c9553b6e86b836e7844824c5a05f556c9391167214469b6318401684b702d7569896bf743c85aee4198612b3315ba778d6 ) -target_compile_definitions(openmw_test_suite +target_compile_definitions(components-tests PRIVATE OPENMW_DATA_DIR=u8"${CMAKE_CURRENT_BINARY_DIR}/data" OPENMW_PROJECT_SOURCE_DIR=u8"${PROJECT_SOURCE_DIR}") -if (MSVC) - target_precompile_headers(openmw_test_suite PRIVATE +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(components-tests PRIVATE diff --git a/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp similarity index 53% rename from apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp rename to apps/components_tests/detournavigator/asyncnavmeshupdater.cpp index 9d8a5d85de..ea9efc3df2 100644 --- a/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp +++ b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp @@ -33,7 +33,8 @@ namespace { const ObjectId id(&shape); osg::ref_ptr bulletShape(new Resource::BulletShape); - bulletShape->mFileName = "test.nif"; + constexpr VFS::Path::NormalizedView test("test.nif"); + bulletShape->mFileName = test; bulletShape->mFileHash = "test_hash"; ObjectTransform objectTransform; std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f); @@ -52,7 +53,7 @@ namespace OffMeshConnectionsManager mOffMeshConnectionsManager{ mSettings.mRecast }; const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; const TilePosition mPlayerTile{ 0, 0 }; - const std::string mWorldspace = "sys::default"; + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("sys::default"); const btBoxShape mBox{ btVector3(100, 100, 20) }; Loading::Listener mListener; }; @@ -266,20 +267,11 @@ namespace updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); updater.wait(WaitConditionType::allJobsDone, &mListener); updater.stop(); - const std::set present{ - TilePosition(-2, 0), - TilePosition(-1, -1), - TilePosition(-1, 0), - TilePosition(-1, 1), - TilePosition(0, -2), - TilePosition(0, -1), - TilePosition(0, 0), - TilePosition(0, 1), - TilePosition(0, 2), - TilePosition(1, -1), - TilePosition(1, 0), - }; + + std::size_t present = 0; + for (int x = -5; x <= 5; ++x) + { for (int y = -5; y <= 5; ++y) { const TilePosition tilePosition(x, y); @@ -289,14 +281,323 @@ namespace recastMesh->getMeshSources(), [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v); }); if (std::holds_alternative(objects)) continue; - EXPECT_EQ(dbPtr - ->findTile(mWorldspace, tilePosition, - serialize(mSettings.mRecast, mAgentBounds, *recastMesh, - std::get>(objects))) - .has_value(), - present.find(tilePosition) != present.end()) - << tilePosition.x() << " " << tilePosition.y() - << " present=" << (present.find(tilePosition) != present.end()); + present += dbPtr + ->findTile(mWorldspace, tilePosition, + serialize(mSettings.mRecast, mAgentBounds, *recastMesh, + std::get>(objects))) + .has_value(); } + } + + EXPECT_EQ(present, 11); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, next_tile_id_should_be_updated_on_duplicate) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + + const TileId nextTileId(dbPtr->getMaxTileId() + 1); + ASSERT_EQ(dbPtr->insertTile(nextTileId, mWorldspace, TilePosition{}, TileVersion{ 1 }, {}, {}), 1); + + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const TilePosition tilePosition{ 0, 0 }; + const std::map changedTiles{ { tilePosition, ChangeType::add } }; + + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + + const AgentBounds agentBounds{ CollisionShapeType::Cylinder, { 29, 29, 66 } }; + updater.post(agentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + + updater.stop(); + + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + ShapeId nextShapeId{ 1 }; + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); }); + const auto tile = dbPtr->findTile( + mWorldspace, tilePosition, serialize(mSettings.mRecast, agentBounds, *recastMesh, objects)); + ASSERT_TRUE(tile.has_value()); + EXPECT_EQ(tile->mTileId, 2); + EXPECT_EQ(tile->mVersion, navMeshFormatVersion); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, repeated_tile_updates_should_be_delayed) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + + mSettings.mMaxTilesNumber = 9; + mSettings.mMinUpdateInterval = std::chrono::milliseconds(250); + + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + + std::map changedTiles; + + for (int x = -3; x <= 3; ++x) + for (int y = -3; y <= 3; ++y) + changedTiles.emplace(TilePosition{ x, y }, ChangeType::update); + + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + + updater.wait(WaitConditionType::allJobsDone, &mListener); + + { + const AsyncNavMeshUpdaterStats stats = updater.getStats(); + EXPECT_EQ(stats.mJobs, 0); + EXPECT_EQ(stats.mWaiting.mDelayed, 0); + } + + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + + { + const AsyncNavMeshUpdaterStats stats = updater.getStats(); + EXPECT_EQ(stats.mJobs, 49); + EXPECT_EQ(stats.mWaiting.mDelayed, 49); + } + + updater.wait(WaitConditionType::allJobsDone, &mListener); + + { + const AsyncNavMeshUpdaterStats stats = updater.getStats(); + EXPECT_EQ(stats.mJobs, 0); + EXPECT_EQ(stats.mWaiting.mDelayed, 0); + } + } + + struct DetourNavigatorSpatialJobQueueTest : Test + { + const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) }; + const std::shared_ptr mNavMeshCacheItemPtr; + const std::weak_ptr mNavMeshCacheItem = mNavMeshCacheItemPtr; + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("worldspace"); + const TilePosition mChangedTile{ 0, 0 }; + const std::chrono::steady_clock::time_point mProcessTime{}; + const TilePosition mPlayerTile{ 0, 0 }; + const int mMaxTiles = 9; + }; + + TEST_F(DetourNavigatorSpatialJobQueueTest, should_store_multiple_jobs_per_tile) + { + std::list jobs; + SpatialJobQueue queue; + + const ESM::RefId worldspace1 = ESM::RefId::stringRefId("worldspace1"); + const ESM::RefId worldspace2 = ESM::RefId::stringRefId("worldspace2"); + + queue.push(jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, worldspace1, mChangedTile, ChangeType::remove, mProcessTime)); + queue.push(jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, worldspace2, mChangedTile, ChangeType::update, mProcessTime)); + + ASSERT_EQ(queue.size(), 2); + + const auto job1 = queue.pop(mChangedTile); + ASSERT_TRUE(job1.has_value()); + EXPECT_EQ((*job1)->mWorldspace, worldspace1); + + const auto job2 = queue.pop(mChangedTile); + ASSERT_TRUE(job2.has_value()); + EXPECT_EQ((*job2)->mWorldspace, worldspace2); + + EXPECT_EQ(queue.size(), 0); + } + + struct DetourNavigatorJobQueueTest : DetourNavigatorSpatialJobQueueTest + { + }; + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_nullptr_from_empty) + { + JobQueue queue; + ASSERT_FALSE(queue.hasJob()); + ASSERT_FALSE(queue.pop(mPlayerTile).has_value()); + } + + TEST_F(DetourNavigatorJobQueueTest, push_on_change_type_remove_should_add_to_removing) + { + const std::chrono::steady_clock::time_point processTime{}; + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::remove, processTime); + + JobQueue queue; + queue.push(job); + + EXPECT_EQ(queue.getStats().mRemoving, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_last_removing) + { + std::list jobs; + JobQueue queue; + + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(0, 0), + ChangeType::remove, mProcessTime)); + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(1, 0), + ChangeType::remove, mProcessTime)); + + ASSERT_TRUE(queue.hasJob()); + const auto job = queue.pop(mPlayerTile); + ASSERT_TRUE(job.has_value()); + EXPECT_EQ((*job)->mChangedTile, TilePosition(1, 0)); + } + + TEST_F(DetourNavigatorJobQueueTest, push_on_change_type_not_remove_should_add_to_updating) + { + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, mProcessTime); + + JobQueue queue; + queue.push(job); + + EXPECT_EQ(queue.getStats().mUpdating, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_nearest_to_player_tile) + { + std::list jobs; + + JobQueue queue; + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(0, 0), + ChangeType::update, mProcessTime)); + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(1, 0), + ChangeType::update, mProcessTime)); + + ASSERT_TRUE(queue.hasJob()); + const auto job = queue.pop(TilePosition(1, 0)); + ASSERT_TRUE(job.has_value()); + EXPECT_EQ((*job)->mChangedTile, TilePosition(1, 0)); + } + + TEST_F(DetourNavigatorJobQueueTest, push_on_processing_time_more_than_now_should_add_to_delayed) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + EXPECT_EQ(queue.getStats().mDelayed, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_when_delayed_job_is_ready) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + ASSERT_FALSE(queue.hasJob(now)); + ASSERT_FALSE(queue.pop(mPlayerTile, now).has_value()); + + ASSERT_TRUE(queue.hasJob(processTime)); + EXPECT_TRUE(queue.pop(mPlayerTile, processTime).has_value()); + } + + TEST_F(DetourNavigatorJobQueueTest, update_should_move_ready_delayed_to_updating) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + ASSERT_EQ(queue.getStats().mDelayed, 1); + + queue.update(mPlayerTile, mMaxTiles, processTime); + + EXPECT_EQ(queue.getStats().mDelayed, 0); + EXPECT_EQ(queue.getStats().mUpdating, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, update_should_move_ready_delayed_to_removing_when_out_of_range) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + ASSERT_EQ(queue.getStats().mDelayed, 1); + + queue.update(TilePosition(10, 10), mMaxTiles, processTime); + + EXPECT_EQ(queue.getStats().mDelayed, 0); + EXPECT_EQ(queue.getStats().mRemoving, 1); + EXPECT_EQ(job->mChangeType, ChangeType::remove); + } + + TEST_F(DetourNavigatorJobQueueTest, update_should_move_updating_to_removing_when_out_of_range) + { + std::list jobs; + + JobQueue queue; + queue.push(jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, mProcessTime)); + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(10, 10), + ChangeType::update, mProcessTime)); + + ASSERT_EQ(queue.getStats().mUpdating, 2); + + queue.update(TilePosition(10, 10), mMaxTiles); + + EXPECT_EQ(queue.getStats().mUpdating, 1); + EXPECT_EQ(queue.getStats().mRemoving, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, clear_should_remove_all) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt removing = jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, + TilePosition(0, 0), ChangeType::remove, mProcessTime); + const JobIt updating = jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, + TilePosition(1, 0), ChangeType::update, mProcessTime); + const JobIt delayed = jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(2, 0), + ChangeType::update, processTime); + + JobQueue queue; + queue.push(removing); + queue.push(updating); + queue.push(delayed, now); + + ASSERT_EQ(queue.getStats().mRemoving, 1); + ASSERT_EQ(queue.getStats().mUpdating, 1); + ASSERT_EQ(queue.getStats().mDelayed, 1); + + queue.clear(); + + EXPECT_EQ(queue.getStats().mRemoving, 0); + EXPECT_EQ(queue.getStats().mUpdating, 0); + EXPECT_EQ(queue.getStats().mDelayed, 0); } } diff --git a/apps/openmw_test_suite/detournavigator/generate.hpp b/apps/components_tests/detournavigator/generate.hpp similarity index 100% rename from apps/openmw_test_suite/detournavigator/generate.hpp rename to apps/components_tests/detournavigator/generate.hpp diff --git a/apps/components_tests/detournavigator/gettilespositions.cpp b/apps/components_tests/detournavigator/gettilespositions.cpp new file mode 100644 index 0000000000..729d11ddb5 --- /dev/null +++ b/apps/components_tests/detournavigator/gettilespositions.cpp @@ -0,0 +1,165 @@ +#include +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + + struct CollectTilesPositions + { + std::vector& mTilesPositions; + + void operator()(const TilePosition& value) { mTilesPositions.push_back(value); } + }; + + struct DetourNavigatorGetTilesPositionsTest : Test + { + RecastSettings mSettings; + std::vector mTilesPositions; + CollectTilesPositions mCollect{ mTilesPositions }; + + DetourNavigatorGetTilesPositionsTest() + { + mSettings.mBorderSize = 0; + mSettings.mCellSize = 0.5; + mSettings.mRecastScaleFactor = 1; + mSettings.mTileSize = 64; + } + }; + + TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_in_single_tile_should_return_one_tile) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(2, 2), osg::Vec2f(31, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_x_bounds_in_two_tiles_should_return_two_tiles) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(32, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(1, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_y_bounds_in_two_tiles_should_return_two_tiles) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31, 32), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(0, 1))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_works_only_for_x_and_y_coordinates) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_should_work_with_negative_coordinates) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(-31, -31), osg::Vec2f(31, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, + ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(0, -1), TilePosition(0, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, border_size_should_extend_tile_bounds) + { + mSettings.mBorderSize = 1; + + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31.5, 31.5), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, + ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(-1, 1), TilePosition(0, -1), + TilePosition(0, 0), TilePosition(0, 1), TilePosition(1, -1), TilePosition(1, 0), TilePosition(1, 1))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, should_apply_recast_scale_factor) + { + mSettings.mRecastScaleFactor = 0.5; + + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(32, 32), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); + } + + struct TilesPositionsRangeParams + { + TilesPositionsRange mA; + TilesPositionsRange mB; + TilesPositionsRange mResult; + }; + + struct DetourNavigatorGetIntersectionTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorGetIntersectionTest, should_return_expected_result) + { + EXPECT_EQ(getIntersection(GetParam().mA, GetParam().mB), GetParam().mResult); + EXPECT_EQ(getIntersection(GetParam().mB, GetParam().mA), GetParam().mResult); + } + + const TilesPositionsRangeParams getIntersectionParams[] = { + { .mA = TilesPositionsRange{}, .mB = TilesPositionsRange{}, .mResult = TilesPositionsRange{} }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 2, 2 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 1, 1 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 0, 2 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 2, 0 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + }; + + INSTANTIATE_TEST_SUITE_P( + GetIntersectionParams, DetourNavigatorGetIntersectionTest, ValuesIn(getIntersectionParams)); + + struct DetourNavigatorGetUnionTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorGetUnionTest, should_return_expected_result) + { + EXPECT_EQ(getUnion(GetParam().mA, GetParam().mB), GetParam().mResult); + EXPECT_EQ(getUnion(GetParam().mB, GetParam().mA), GetParam().mResult); + } + + const TilesPositionsRangeParams getUnionParams[] = { + { .mA = TilesPositionsRange{}, .mB = TilesPositionsRange{}, .mResult = TilesPositionsRange{} }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 3, 3 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + }, + }; + + INSTANTIATE_TEST_SUITE_P(GetUnionParams, DetourNavigatorGetUnionTest, ValuesIn(getUnionParams)); +} diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp similarity index 68% rename from apps/openmw_test_suite/detournavigator/navigator.cpp rename to apps/components_tests/detournavigator/navigator.cpp index a93693c08b..2927869782 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -39,37 +39,44 @@ namespace using namespace DetourNavigator; using namespace DetourNavigator::Tests; + constexpr int heightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); + struct DetourNavigatorNavigatorTest : Test { Settings mSettings = makeSettings(); - std::unique_ptr mNavigator; - const osg::Vec3f mPlayerPosition; - const std::string mWorldspace; + std::unique_ptr mNavigator = std::make_unique( + mSettings, std::make_unique(":memory:", std::numeric_limits::max())); + const osg::Vec3f mPlayerPosition{ 256, 256, 0 }; + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("sys::default"); const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; - osg::Vec3f mStart; - osg::Vec3f mEnd; + osg::Vec3f mStart{ 52, 460, 1 }; + osg::Vec3f mEnd{ 460, 52, 1 }; std::deque mPath; - std::back_insert_iterator> mOut; + std::back_insert_iterator> mOut{ mPath }; AreaCosts mAreaCosts; Loading::Listener mListener; const osg::Vec2i mCellPosition{ 0, 0 }; - const int mHeightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); const float mEndTolerance = 0; const btTransform mTransform{ btMatrix3x3::getIdentity(), btVector3(256, 256, 0) }; const ObjectTransform mObjectTransform{ ESM::Position{ { 256, 256, 0 }, { 0, 0, 0 } }, 0.0f }; - - DetourNavigatorNavigatorTest() - : mPlayerPosition(256, 256, 0) - , mWorldspace("sys::default") - , mStart(52, 460, 1) - , mEnd(460, 52, 1) - , mOut(mPath) - { - mNavigator.reset(new NavigatorImpl( - mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - } }; + constexpr std::array defaultHeightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + + constexpr std::array defaultHeightfieldDataScalar{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + template std::unique_ptr makeSquareHeightfieldTerrainShape( const std::array& values, btScalar heightScale = 1, int upAxis = 2, @@ -113,7 +120,7 @@ namespace { } - T& shape() { return static_cast(*mInstance->mCollisionShape); } + T& shape() const { return static_cast(*mInstance->mCollisionShape); } const osg::ref_ptr& instance() const { return mInstance; } private: @@ -150,15 +157,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) { - constexpr std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); auto updateGuard = mNavigator->makeUpdateGuard(); @@ -177,21 +177,32 @@ namespace << mPath; } + TEST_F(DetourNavigatorNavigatorTest, find_path_to_the_start_position_should_contain_single_point) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; + } + TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh) { mSettings.mWaitUntilMinDistanceToPlayer = 0; mNavigator.reset(new NavigatorImpl( mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape( @@ -235,15 +246,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape( @@ -288,14 +292,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher) { - const std::array heightfieldData1{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(heightfieldData1)); + CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); heightfield1.shape().setLocalScaling(btVector3(128, 128, 1)); const std::array heightfieldData2{ { @@ -328,15 +325,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) { - const std::array heightfieldData1{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1); - const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1); + const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize1 = heightfieldTileSize * (surface1.mSize - 1); const std::array heightfieldData2{ { -25, -25, -25, -25, -25, // row 0 @@ -346,7 +336,7 @@ namespace -25, -25, -25, -25, -25, // row 4 } }; const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2); - const int cellSize2 = mHeightfieldTileSize * (surface2.mSize - 1); + const int cellSize2 = heightfieldTileSize * (surface2.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize1, surface1, nullptr); @@ -366,14 +356,8 @@ namespace { osg::ref_ptr bulletShape(new Resource::BulletShape); - std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); + std::unique_ptr shapePtr + = makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar); shapePtr->setLocalScaling(btVector3(128, 128, 1)); bulletShape->mCollisionShape.reset(shapePtr.release()); @@ -419,7 +403,7 @@ namespace 0, -50, -100, -100, -100, // row 4 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addWater(mCellPosition, cellSize, 300, nullptr); @@ -453,7 +437,7 @@ namespace 0, 0, 0, 0, 0, 0, 0, // row 6 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addWater(mCellPosition, cellSize, -25, nullptr); @@ -487,7 +471,7 @@ namespace 0, 0, 0, 0, 0, 0, 0, // row 6 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -520,7 +504,7 @@ namespace 0, 0, 0, 0, 0, 0, 0, // row 6 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addWater(mCellPosition, cellSize, -25, nullptr); @@ -542,14 +526,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(heightfieldData)); + CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -579,15 +556,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -623,7 +593,7 @@ namespace 0, -25, -100, -100, -100, -100, // row 5 } }; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -649,15 +619,8 @@ namespace mNavigator.reset(new NavigatorImpl( mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); std::vector> boxes; @@ -745,15 +708,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); @@ -771,15 +727,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); const btVector3 oscillatingBoxShapePosition(288, 288, 400); @@ -819,7 +768,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield) { const HeightfieldPlane plane{ 100 }; - const int cellSize = mHeightfieldTileSize * 4; + const int cellSize = heightfieldTileSize * 4; ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addHeightfield(mCellPosition, cellSize, plane, nullptr); @@ -837,15 +786,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), @@ -870,15 +812,8 @@ namespace TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), @@ -924,16 +859,29 @@ namespace EXPECT_EQ(mNavigator->getNavMesh(mAgentBounds)->lockConst()->getVersion(), version); } + std::pair getMinMax(const RecastMeshTiles& tiles) + { + const auto lessByX = [](const auto& l, const auto& r) { return l.first.x() < r.first.x(); }; + const auto lessByY = [](const auto& l, const auto& r) { return l.first.y() < r.first.y(); }; + + const auto [minX, maxX] = std::ranges::minmax_element(tiles, lessByX); + const auto [minY, maxY] = std::ranges::minmax_element(tiles, lessByY); + + return { TilePosition(minX->first.x(), minY->first.y()), TilePosition(maxX->first.x(), maxY->first.y()) }; + } + TEST_F(DetourNavigatorNavigatorTest, update_for_very_big_object_should_be_limited) { - const float size = static_cast(2 * static_cast(std::numeric_limits::max()) - 1); + const float size = static_cast((1 << 22) - 1); CollisionShapeInstance bigBox(std::make_unique(btVector3(size, size, 1))); const ObjectTransform objectTransform{ .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, .mScale = 1.0f, }; + const std::optional cellGridBounds = std::nullopt; + const osg::Vec3f playerPosition(32, 1024, 0); - mNavigator->updateBounds(mPlayerPosition, nullptr); + mNavigator->updateBounds(mWorldspace, cellGridBounds, playerPosition, nullptr); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->addObject(ObjectId(&bigBox.shape()), ObjectShapes(bigBox.instance(), objectTransform), btTransform::getIdentity(), nullptr); @@ -943,7 +891,8 @@ namespace std::mutex mutex; std::thread thread([&] { - mNavigator->update(mPlayerPosition, nullptr); + auto guard = mNavigator->makeUpdateGuard(); + mNavigator->update(playerPosition, guard.get()); std::lock_guard lock(mutex); updated = true; updateFinished.notify_all(); @@ -951,7 +900,7 @@ namespace { std::unique_lock lock(mutex); - updateFinished.wait_for(lock, std::chrono::seconds(3), [&] { return updated; }); + updateFinished.wait_for(lock, std::chrono::seconds(10), [&] { return updated; }); ASSERT_TRUE(updated); } @@ -959,14 +908,69 @@ namespace mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(mNavigator->getRecastMeshTiles().size(), 509); + const auto recastMeshTiles = mNavigator->getRecastMeshTiles(); + ASSERT_EQ(recastMeshTiles.size(), 1033); + EXPECT_EQ(getMinMax(recastMeshTiles), std::pair(TilePosition(-18, -17), TilePosition(18, 19))); const auto navMesh = mNavigator->getNavMesh(mAgentBounds); ASSERT_NE(navMesh, nullptr); std::size_t usedNavMeshTiles = 0; navMesh->lockConst()->forEachUsedTile([&](const auto&...) { ++usedNavMeshTiles; }); - EXPECT_EQ(usedNavMeshTiles, 509); + EXPECT_EQ(usedNavMeshTiles, 1024); + } + + TEST_F(DetourNavigatorNavigatorTest, update_should_be_limited_by_cell_grid_bounds) + { + const float size = static_cast((1 << 22) - 1); + CollisionShapeInstance bigBox(std::make_unique(btVector3(size, size, 1))); + const ObjectTransform objectTransform{ + .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, + .mScale = 1.0f, + }; + const CellGridBounds cellGridBounds{ + .mCenter = osg::Vec2i(0, 0), + .mHalfSize = 1, + }; + const osg::Vec3f playerPosition(32, 1024, 0); + + mNavigator->updateBounds(mWorldspace, cellGridBounds, playerPosition, nullptr); + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addObject(ObjectId(&bigBox.shape()), ObjectShapes(bigBox.instance(), objectTransform), + btTransform::getIdentity(), nullptr); + + bool updated = false; + std::condition_variable updateFinished; + std::mutex mutex; + + std::thread thread([&] { + auto guard = mNavigator->makeUpdateGuard(); + mNavigator->update(playerPosition, guard.get()); + std::lock_guard lock(mutex); + updated = true; + updateFinished.notify_all(); + }); + + { + std::unique_lock lock(mutex); + updateFinished.wait_for(lock, std::chrono::seconds(10), [&] { return updated; }); + ASSERT_TRUE(updated); + } + + thread.join(); + + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const auto recastMeshTiles = mNavigator->getRecastMeshTiles(); + ASSERT_EQ(recastMeshTiles.size(), 854); + EXPECT_EQ(getMinMax(recastMeshTiles), std::pair(TilePosition(-12, -12), TilePosition(18, 19))); + + const auto navMesh = mNavigator->getNavMesh(mAgentBounds); + ASSERT_NE(navMesh, nullptr); + + std::size_t usedNavMeshTiles = 0; + navMesh->lockConst()->forEachUsedTile([&](const auto&...) { ++usedNavMeshTiles; }); + EXPECT_EQ(usedNavMeshTiles, 854); } struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam @@ -1000,4 +1004,286 @@ namespace INSTANTIATE_TEST_SUITE_P(NotSupportedAgentBounds, DetourNavigatorNavigatorNotSupportedAgentBoundsTest, ValuesIn(notSupportedAgentBounds)); + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nav_mesh_position) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_THAT(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + Optional(Vec3fEq(250, 250, -62.5186))); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_too_far) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 250); + const osg::Vec3f searchAreaHalfExtents(100, 100, 100); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + std::nullopt); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_flags_do_not_match) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_swim), + std::nullopt); + } + + struct DetourNavigatorUpdateTest : TestWithParam> + { + }; + + std::vector getUsedTiles(const NavMeshCacheItem& navMesh) + { + std::vector result; + navMesh.forEachUsedTile([&](const TilePosition& position, const auto&...) { result.push_back(position); }); + return result; + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 5; + NavigatorImpl navigator(settings, nullptr); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::allJobsDone, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTiles[] = { { 3, 4 }, { 4, 3 }, { 4, 4 }, { 4, 5 }, { 5, 4 } }; + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, UnorderedElementsAreArray(expectedTiles)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(4000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::allJobsDone, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTiles[] = { { 4, 4 }, { 5, 3 }, { 5, 4 }, { 5, 5 }, { 6, 4 } }; + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, UnorderedElementsAreArray(expectedTiles)) << usedTiles; + } + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves_without_waiting_for_all) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 1; + settings.mWaitUntilMinDistanceToPlayer = 1; + NavigatorImpl navigator(settings, nullptr); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(4, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(6000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(8, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves_with_db) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 1; + settings.mWaitUntilMinDistanceToPlayer = 1; + NavigatorImpl navigator(settings, std::make_unique(":memory:", settings.mMaxDbFileSize)); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(4, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(6000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(8, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + } + + struct AddHeightfieldSurface + { + static constexpr std::size_t sSize = 65; + static constexpr float sHeights[sSize * sSize]{}; + + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const HeightfieldSurface surface{ + .mHeights = sHeights, + .mSize = sSize, + .mMinHeight = -1, + .mMaxHeight = 1, + }; + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + navigator.addHeightfield(cellPosition, cellSize, surface, nullptr); + } + }; + + struct AddHeightfieldPlane + { + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const HeightfieldPlane plane{ .mHeight = 0 }; + const int cellSize = 8192; + navigator.addHeightfield(cellPosition, cellSize, plane, nullptr); + } + }; + + struct AddWater + { + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const float level = 0; + const int cellSize = 8192; + navigator.addWater(cellPosition, cellSize, level, nullptr); + } + }; + + struct AddObject + { + const float mSize = 8192; + CollisionShapeInstance mBox{ std::make_unique(btVector3(mSize, mSize, 1)) }; + const ObjectTransform mTransform{ + .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, + .mScale = 1.0f, + }; + + void operator()(Navigator& navigator) const + { + navigator.addObject(ObjectId(&mBox.shape()), ObjectShapes(mBox.instance(), mTransform), + btTransform::getIdentity(), nullptr); + } + }; + + struct AddAll + { + AddHeightfieldSurface mAddHeightfieldSurface; + AddHeightfieldPlane mAddHeightfieldPlane; + AddWater mAddWater; + AddObject mAddObject; + + void operator()(Navigator& navigator) const + { + mAddHeightfieldSurface(navigator); + mAddHeightfieldPlane(navigator); + mAddWater(navigator); + mAddObject(navigator); + } + }; + + const std::function addNavMeshData[] = { + AddHeightfieldSurface{}, + AddHeightfieldPlane{}, + AddWater{}, + AddObject{}, + AddAll{}, + }; + + INSTANTIATE_TEST_SUITE_P(DifferentNavMeshData, DetourNavigatorUpdateTest, ValuesIn(addNavMeshData)); } diff --git a/apps/openmw_test_suite/detournavigator/navmeshdb.cpp b/apps/components_tests/detournavigator/navmeshdb.cpp similarity index 92% rename from apps/openmw_test_suite/detournavigator/navmeshdb.cpp rename to apps/components_tests/detournavigator/navmeshdb.cpp index 4cf88f5711..cd74983b0e 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshdb.cpp +++ b/apps/components_tests/detournavigator/navmeshdb.cpp @@ -8,7 +8,6 @@ #include #include -#include #include namespace @@ -19,7 +18,7 @@ namespace struct Tile { - std::string mWorldspace; + ESM::RefId mWorldspace; TilePosition mTilePosition; std::vector mInput; std::vector mData; @@ -39,12 +38,12 @@ namespace Tile insertTile(TileId tileId, TileVersion version) { - std::string worldspace = "sys::default"; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); const TilePosition tilePosition{ 3, 4 }; std::vector input = generateData(); std::vector data = generateData(); EXPECT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); - return { std::move(worldspace), tilePosition, std::move(input), std::move(data) }; + return { worldspace, tilePosition, std::move(input), std::move(data) }; } }; @@ -89,7 +88,7 @@ namespace { const TileId tileId{ 53 }; const TileVersion version{ 1 }; - const std::string worldspace = "sys::default"; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); const TilePosition tilePosition{ 3, 4 }; const std::vector input = generateData(); const std::vector data = generateData(); @@ -101,7 +100,7 @@ namespace { const TileId tileId{ 53 }; const TileVersion version{ 1 }; - const std::string worldspace = "sys::default"; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); const TilePosition tilePosition{ 3, 4 }; const std::vector input = generateData(); const std::vector data = generateData(); @@ -113,7 +112,7 @@ namespace TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_at_should_remove_all_tiles_with_given_worldspace_and_position) { const TileVersion version{ 1 }; - const std::string worldspace = "sys::default"; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); const TilePosition tilePosition{ 3, 4 }; const std::vector input1 = generateData(); const std::vector input2 = generateData(); @@ -130,7 +129,7 @@ namespace const TileId leftTileId{ 53 }; const TileId removedTileId{ 54 }; const TileVersion version{ 1 }; - const std::string worldspace = "sys::default"; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); const TilePosition tilePosition{ 3, 4 }; const std::vector leftInput = generateData(); const std::vector removedInput = generateData(); @@ -148,7 +147,7 @@ namespace { TileId tileId{ 1 }; const TileVersion version{ 1 }; - const std::string worldspace = "sys::default"; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); const std::vector input = generateData(); const std::vector data = generateData(); for (int x = -2; x <= 2; ++x) diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/components_tests/detournavigator/navmeshtilescache.cpp similarity index 100% rename from apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp rename to apps/components_tests/detournavigator/navmeshtilescache.cpp diff --git a/apps/openmw_test_suite/detournavigator/operators.hpp b/apps/components_tests/detournavigator/operators.hpp similarity index 82% rename from apps/openmw_test_suite/detournavigator/operators.hpp rename to apps/components_tests/detournavigator/operators.hpp index 4e42af78e4..4c043027eb 100644 --- a/apps/openmw_test_suite/detournavigator/operators.hpp +++ b/apps/components_tests/detournavigator/operators.hpp @@ -42,12 +42,24 @@ namespace testing << ", " << value.y() << ", " << value.z() << ')'; } + template <> + inline testing::Message& Message::operator<<(const osg::Vec2i& value) + { + return (*this) << "{" << value.x() << ", " << value.y() << '}'; + } + template <> inline testing::Message& Message::operator<<(const Wrapper& value) { return (*this) << value.mValue; } + template <> + inline testing::Message& Message::operator<<(const Wrapper& value) + { + return (*this) << value.mValue; + } + template <> inline testing::Message& Message::operator<<(const Wrapper& value) { @@ -72,6 +84,12 @@ namespace testing return writeRange(*this, value, 1); } + template <> + inline testing::Message& Message::operator<<(const std::vector& value) + { + return writeRange(*this, value, 1); + } + template <> inline testing::Message& Message::operator<<(const std::vector& value) { diff --git a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp b/apps/components_tests/detournavigator/recastmeshbuilder.cpp similarity index 100% rename from apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp rename to apps/components_tests/detournavigator/recastmeshbuilder.cpp diff --git a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp b/apps/components_tests/detournavigator/recastmeshobject.cpp similarity index 100% rename from apps/openmw_test_suite/detournavigator/recastmeshobject.cpp rename to apps/components_tests/detournavigator/recastmeshobject.cpp diff --git a/apps/openmw_test_suite/detournavigator/settings.hpp b/apps/components_tests/detournavigator/settings.hpp similarity index 97% rename from apps/openmw_test_suite/detournavigator/settings.hpp rename to apps/components_tests/detournavigator/settings.hpp index dc37dc7550..1ebbc5ba7b 100644 --- a/apps/openmw_test_suite/detournavigator/settings.hpp +++ b/apps/components_tests/detournavigator/settings.hpp @@ -39,7 +39,7 @@ namespace DetourNavigator result.mDetour.mMaxPolygonPathSize = 1024; result.mDetour.mMaxSmoothPathSize = 1024; result.mDetour.mMaxPolys = 4096; - result.mMaxTilesNumber = 512; + result.mMaxTilesNumber = 1024; result.mMinUpdateInterval = std::chrono::milliseconds(50); result.mWriteToNavMeshDb = true; return result; diff --git a/apps/openmw_test_suite/detournavigator/settingsutils.cpp b/apps/components_tests/detournavigator/settingsutils.cpp similarity index 100% rename from apps/openmw_test_suite/detournavigator/settingsutils.cpp rename to apps/components_tests/detournavigator/settingsutils.cpp diff --git a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp similarity index 80% rename from apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp rename to apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp index df695ec254..e1805e993c 100644 --- a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp @@ -21,6 +21,7 @@ namespace const ObjectTransform mObjectTransform{ ESM::Position{ { 0, 0, 0 }, { 0, 0, 0 } }, 0.0f }; const osg::ref_ptr mShape = new Resource::BulletShape; const osg::ref_ptr mInstance = new Resource::BulletShapeInstance(mShape); + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("worldspace"); DetourNavigatorTileCachedRecastMeshManagerTest() { @@ -34,7 +35,7 @@ namespace TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr) { TileCachedRecastMeshManager manager(mSettings); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_for_empty_should_return_zero) @@ -65,14 +66,14 @@ namespace TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_add_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); ASSERT_TRUE(manager.addObject( ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); for (int x = -1; x < 1; ++x) for (int y = -1; y < 1; ++y) - ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_return_add_changed_tiles) @@ -145,25 +146,25 @@ namespace get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); } TEST_F( DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_nullptr_for_unused_tile) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, @@ -172,10 +173,10 @@ namespace TileCachedRecastMeshManager manager(mSettings); const TilesPositionsRange range{ .mBegin = TilePosition(-1, -1), - .mEnd = TilePosition(1, 1), + .mEnd = TilePosition(2, 2), }; manager.setRange(range, nullptr); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform( @@ -183,23 +184,23 @@ namespace const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(1, -1)), nullptr); manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); } TEST_F( DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_moved_object_should_return_nullptr_for_unused_tile) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform( @@ -207,48 +208,48 @@ namespace const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, -1)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_removed_object_should_return_nullptr_for_all_previously_used_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); manager.removeObject(ObjectId(&boxShape), nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr); - EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_not_changed_object_after_update_should_return_recast_mesh_for_same_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr); - EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, @@ -293,7 +294,7 @@ namespace get_revision_after_update_not_changed_object_should_return_same_value) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); @@ -339,19 +340,19 @@ namespace TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; manager.addWater(cellPosition, cellSize, 0.0f, nullptr); for (int x = -1; x < 12; ++x) for (int y = -1; y < 12; ++y) - ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_max_int_should_not_add_new_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); ASSERT_TRUE(manager.addObject( @@ -361,7 +362,7 @@ namespace manager.addWater(cellPosition, cellSize, 0.0f, nullptr); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) - ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)) != nullptr, + ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0); } @@ -390,20 +391,20 @@ namespace TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; manager.addWater(cellPosition, cellSize, 0.0f, nullptr); manager.removeWater(cellPosition, nullptr); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) - ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_leave_not_empty_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(mInstance, boxShape, mObjectTransform); ASSERT_TRUE(manager.addObject( @@ -414,14 +415,14 @@ namespace manager.removeWater(cellPosition, nullptr); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) - ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)) != nullptr, + ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_object_should_not_remove_tile_with_water) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; const btBoxShape boxShape(btVector3(20, 20, 100)); @@ -432,24 +433,25 @@ namespace manager.removeObject(ObjectId(&boxShape), nullptr); for (int x = -1; x < 12; ++x) for (int y = -1; y < 12; ++y) - ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr); + ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_new_worldspace_should_remove_tiles) { TileCachedRecastMeshManager manager(mSettings); - manager.setWorldspace("worldspace", nullptr); + manager.setWorldspace(mWorldspace, nullptr); const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(nullptr, boxShape, mObjectTransform); ASSERT_TRUE(manager.addObject( ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); - manager.setWorldspace("other", nullptr); + const ESM::RefId otherWorldspace(ESM::FormId::fromUint32(0x1)); + manager.setWorldspace(ESM::FormId::fromUint32(0x1), nullptr); for (int x = -1; x < 1; ++x) for (int y = -1; y < 1; ++y) - ASSERT_EQ(manager.getMesh("other", TilePosition(x, y)), nullptr); + ASSERT_EQ(manager.getMesh(otherWorldspace, TilePosition(x, y)), nullptr); } - TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_bounds_should_add_changed_tiles) + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_range_should_add_changed_tiles) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); @@ -470,4 +472,35 @@ namespace ElementsAre( std::pair(TilePosition(-1, -1), ChangeType::add), std::pair(TilePosition(0, 0), ChangeType::remove))); } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_range_should_remove_cached_recast_meshes_outside_range) + { + TileCachedRecastMeshManager manager(mSettings); + + manager.setWorldspace(mWorldspace, nullptr); + + const btBoxShape boxShape(btVector3(100, 100, 20)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + const TilesPositionsRange range1{ + .mBegin = TilePosition(0, 0), + .mEnd = TilePosition(1, 1), + }; + manager.setRange(range1, nullptr); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + + const TilePosition tilePosition(0, 0); + + ASSERT_EQ(manager.getCachedMesh(mWorldspace, tilePosition), nullptr); + ASSERT_NE(manager.getMesh(mWorldspace, tilePosition), nullptr); + ASSERT_NE(manager.getCachedMesh(mWorldspace, tilePosition), nullptr); + + const TilesPositionsRange range2{ + .mBegin = TilePosition(-1, -1), + .mEnd = TilePosition(0, 0), + }; + manager.takeChangedTiles(nullptr); + manager.setRange(range2, nullptr); + + ASSERT_EQ(manager.getCachedMesh(mWorldspace, tilePosition), nullptr); + } } diff --git a/apps/openmw_test_suite/esm/test_fixed_string.cpp b/apps/components_tests/esm/test_fixed_string.cpp similarity index 100% rename from apps/openmw_test_suite/esm/test_fixed_string.cpp rename to apps/components_tests/esm/test_fixed_string.cpp diff --git a/apps/openmw_test_suite/esm/testrefid.cpp b/apps/components_tests/esm/testrefid.cpp similarity index 99% rename from apps/openmw_test_suite/esm/testrefid.cpp rename to apps/components_tests/esm/testrefid.cpp index 168f9e0b9e..1911cd1a5a 100644 --- a/apps/openmw_test_suite/esm/testrefid.cpp +++ b/apps/components_tests/esm/testrefid.cpp @@ -1,6 +1,7 @@ -#include "components/esm/refid.hpp" -#include "components/esm3/esmreader.hpp" -#include "components/esm3/esmwriter.hpp" +#include +#include +#include +#include #include #include @@ -9,8 +10,6 @@ #include #include -#include "../testing_util.hpp" - MATCHER(IsPrint, "") { return std::isprint(arg) != 0; diff --git a/apps/openmw_test_suite/esm/variant.cpp b/apps/components_tests/esm/variant.cpp similarity index 100% rename from apps/openmw_test_suite/esm/variant.cpp rename to apps/components_tests/esm/variant.cpp diff --git a/apps/openmw_test_suite/esm3/readerscache.cpp b/apps/components_tests/esm3/readerscache.cpp similarity index 100% rename from apps/openmw_test_suite/esm3/readerscache.cpp rename to apps/components_tests/esm3/readerscache.cpp diff --git a/apps/components_tests/esm3/testcstringids.cpp b/apps/components_tests/esm3/testcstringids.cpp new file mode 100644 index 0000000000..239b205965 --- /dev/null +++ b/apps/components_tests/esm3/testcstringids.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +#include + +namespace ESM +{ + namespace + { + TEST(Esm3CStringIdTest, dialNameShouldBeNullTerminated) + { + std::unique_ptr stream; + + { + auto ostream = std::make_unique(); + + ESMWriter writer; + writer.setFormatVersion(DefaultFormatVersion); + writer.save(*ostream); + + Dialogue record; + record.blank(); + record.mStringId = "topic name"; + record.mId = RefId::stringRefId(record.mStringId); + record.mType = Dialogue::Topic; + writer.startRecord(Dialogue::sRecordId); + record.save(writer); + writer.endRecord(Dialogue::sRecordId); + + stream = std::move(ostream); + } + + ESMReader reader; + reader.open(std::move(stream), "stream"); + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName(), Dialogue::sRecordId); + reader.getRecHeader(); + while (reader.hasMoreSubs()) + { + reader.getSubName(); + if (reader.retSubName().toInt() == SREC_NAME) + { + reader.getSubHeader(); + auto size = reader.getSubSize(); + std::string buffer(size, '1'); + reader.getExact(buffer.data(), size); + ASSERT_EQ(buffer[size - 1], '\0'); + return; + } + else + reader.skipHSub(); + } + ASSERT_FALSE(true); + } + } +} diff --git a/apps/openmw_test_suite/esm3/testesmwriter.cpp b/apps/components_tests/esm3/testesmwriter.cpp similarity index 99% rename from apps/openmw_test_suite/esm3/testesmwriter.cpp rename to apps/components_tests/esm3/testesmwriter.cpp index 9e9ae9947e..c481684c8d 100644 --- a/apps/openmw_test_suite/esm3/testesmwriter.cpp +++ b/apps/components_tests/esm3/testesmwriter.cpp @@ -57,7 +57,7 @@ namespace ESM // If this test failed probably there is a change in RefId format and CurrentSaveGameFormatVersion should be // incremented, current version should be handled. - TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNubmerOfBytes) + TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNumberOfBytes) { const auto [refId, size] = GetParam(); diff --git a/apps/openmw_test_suite/esm3/testinfoorder.cpp b/apps/components_tests/esm3/testinfoorder.cpp similarity index 100% rename from apps/openmw_test_suite/esm3/testinfoorder.cpp rename to apps/components_tests/esm3/testinfoorder.cpp diff --git a/apps/components_tests/esm3/testsaveload.cpp b/apps/components_tests/esm3/testsaveload.cpp new file mode 100644 index 0000000000..41a79313cc --- /dev/null +++ b/apps/components_tests/esm3/testsaveload.cpp @@ -0,0 +1,762 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace ESM +{ + namespace + { + auto tie(const ContItem& value) + { + return std::tie(value.mCount, value.mItem); + } + + auto tie(const ESM::Region::SoundRef& value) + { + return std::tie(value.mSound, value.mChance); + } + + auto tie(const ESM::QuickKeys::QuickKey& value) + { + return std::tie(value.mType, value.mId); + } + } + + inline bool operator==(const ESM::ContItem& lhs, const ESM::ContItem& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline std::ostream& operator<<(std::ostream& stream, const ESM::ContItem& value) + { + return stream << "ESM::ContItem {.mCount = " << value.mCount << ", .mItem = '" << value.mItem << "'}"; + } + + inline bool operator==(const ESM::Region::SoundRef& lhs, const ESM::Region::SoundRef& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline std::ostream& operator<<(std::ostream& stream, const ESM::Region::SoundRef& value) + { + return stream << "ESM::Region::SoundRef {.mSound = '" << value.mSound << "', .mChance = " << value.mChance + << "}"; + } + + inline bool operator==(const ESM::QuickKeys::QuickKey& lhs, const ESM::QuickKeys::QuickKey& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline std::ostream& operator<<(std::ostream& stream, const ESM::QuickKeys::QuickKey& value) + { + return stream << "ESM::QuickKeys::QuickKey {.mType = '" << static_cast(value.mType) + << "', .mId = " << value.mId << "}"; + } + + namespace + { + using namespace ::testing; + + std::vector getFormats() + { + std::vector result({ + CurrentContentFormatVersion, + MaxLimitedSizeStringsFormatVersion, + MaxStringRefIdFormatVersion, + }); + for (ESM::FormatVersion v = result.back() + 1; v <= ESM::CurrentSaveGameFormatVersion; ++v) + result.push_back(v); + return result; + } + + constexpr std::uint32_t fakeRecordId = fourCC("FAKE"); + + template + concept HasSave = requires(T v, ESMWriter& w) + { + v.save(w); + }; + + template + concept NotHasSave = !HasSave; + + template + auto save(const T& record, ESMWriter& writer) + { + record.save(writer); + } + + void save(const CellRef& record, ESMWriter& writer) + { + record.save(writer, true); + } + + template + auto save(const T& record, ESMWriter& writer) + { + writer.writeComposite(record); + } + + template + std::unique_ptr makeEsmStream(const T& record, FormatVersion formatVersion) + { + ESMWriter writer; + auto stream = std::make_unique(); + writer.setFormatVersion(formatVersion); + writer.save(*stream); + writer.startRecord(fakeRecordId); + save(record, writer); + writer.endRecord(fakeRecordId); + return stream; + } + + template + concept HasLoad = requires(T v, ESMReader& r) + { + v.load(r); + }; + + template + concept HasLoadWithDelete = requires(T v, ESMReader& r, bool& d) + { + v.load(r, d); + }; + + template + concept NotHasLoad = !HasLoad && !HasLoadWithDelete; + + template + void load(ESMReader& reader, T& record) + { + record.load(reader); + } + + template + void load(ESMReader& reader, T& record) + { + bool deleted = false; + record.load(reader, deleted); + } + + void load(ESMReader& reader, CellRef& record) + { + bool deleted = false; + record.load(reader, deleted, true); + } + + template + void load(ESMReader& reader, T& record) + { + reader.getComposite(record); + } + + void load(ESMReader& reader, Land& record) + { + bool deleted = false; + record.load(reader, deleted); + if (deleted) + return; + record.mLandData = std::make_unique(); + reader.restoreContext(record.mContext); + loadLandRecordData(record.mDataTypes, reader, *record.mLandData); + } + + template + void saveAndLoadRecord(const T& record, FormatVersion formatVersion, T& result) + { + ESMReader reader; + reader.open(makeEsmStream(record, formatVersion), "stream"); + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName().toInt(), fakeRecordId); + reader.getRecHeader(); + load(reader, result); + } + + struct Esm3SaveLoadRecordTest : public TestWithParam + { + std::minstd_rand mRandom; + std::uniform_int_distribution mRefIdDistribution{ 'a', 'z' }; + + std::string generateRandomString(std::size_t size) + { + std::string value; + while (value.size() < size) + value.push_back(static_cast(mRefIdDistribution(mRandom))); + return value; + } + + RefId generateRandomRefId(std::size_t size = 33) { return RefId::stringRefId(generateRandomString(size)); } + + template + void generateArray(T (&dst)[n]) + { + for (auto& v : dst) + v = std::uniform_real_distribution{ -1.0f, 1.0f }(mRandom); + } + + void generateBytes(auto iterator, std::size_t count) + { + std::uniform_int_distribution distribution{ 0, + std::numeric_limits::max() }; + std::generate_n(iterator, count, [&] { return static_cast(distribution(mRandom)); }); + } + + void generateStrings(auto iterator, std::size_t count) + { + std::uniform_int_distribution distribution{ 1, 13 }; + std::generate_n(iterator, count, [&] { return generateRandomString(distribution(mRandom)); }); + } + }; + + TEST_F(Esm3SaveLoadRecordTest, headerShouldNotChange) + { + const std::string author = generateRandomString(33); + const std::string description = generateRandomString(257); + + auto stream = std::make_unique(); + + ESMWriter writer; + writer.setAuthor(author); + writer.setDescription(description); + writer.setFormatVersion(CurrentSaveGameFormatVersion); + writer.save(*stream); + writer.close(); + + ESMReader reader; + reader.open(std::move(stream), "stream"); + EXPECT_EQ(reader.getAuthor(), author); + EXPECT_EQ(reader.getDesc(), description); + } + + TEST_F(Esm3SaveLoadRecordTest, containerContItemShouldSupportRefIdLongerThan32) + { + Container record; + record.blank(); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 42, .mItem = generateRandomRefId(33) }); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 13, .mItem = generateRandomRefId(33) }); + Container result; + saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); + EXPECT_EQ(result.mInventory.mList, record.mInventory.mList); + } + + TEST_F(Esm3SaveLoadRecordTest, regionSoundRefShouldSupportRefIdLongerThan32) + { + Region record; + record.blank(); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(33), .mChance = 42 }); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(33), .mChance = 13 }); + Region result; + saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); + EXPECT_EQ(result.mSoundList, record.mSoundList); + } + + TEST_F(Esm3SaveLoadRecordTest, scriptSoundRefShouldSupportRefIdLongerThan32) + { + Script record; + record.blank(); + record.mId = generateRandomRefId(33); + record.mNumShorts = 42; + Script result; + saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mNumShorts, record.mNumShorts); + } + + TEST_P(Esm3SaveLoadRecordTest, playerShouldNotChange) + { + // Player state is not saved to vanilla ESM format. + if (GetParam() == CurrentContentFormatVersion) + return; + std::minstd_rand random; + Player record{}; + record.mObject.blank(); + record.mBirthsign = generateRandomRefId(); + record.mObject.mRef.mRefID = generateRandomRefId(); + std::generate_n(std::inserter(record.mPreviousItems, record.mPreviousItems.end()), 2, + [&] { return std::make_pair(generateRandomRefId(), generateRandomRefId()); }); + record.mCellId = ESM::RefId::esm3ExteriorCell(0, 0); + generateArray(record.mLastKnownExteriorPosition); + record.mHasMark = true; + record.mMarkedCell = ESM::RefId::esm3ExteriorCell(0, 0); + generateArray(record.mMarkedPosition.pos); + generateArray(record.mMarkedPosition.rot); + record.mCurrentCrimeId = 42; + record.mPaidCrimeId = 13; + Player result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(record.mObject.mRef.mRefID, result.mObject.mRef.mRefID); + EXPECT_EQ(record.mBirthsign, result.mBirthsign); + EXPECT_EQ(record.mPreviousItems, result.mPreviousItems); + EXPECT_EQ(record.mPreviousItems, result.mPreviousItems); + EXPECT_EQ(record.mCellId, result.mCellId); + EXPECT_THAT(record.mLastKnownExteriorPosition, ElementsAreArray(result.mLastKnownExteriorPosition)); + EXPECT_EQ(record.mHasMark, result.mHasMark); + EXPECT_EQ(record.mMarkedCell, result.mMarkedCell); + EXPECT_THAT(record.mMarkedPosition.pos, ElementsAreArray(result.mMarkedPosition.pos)); + EXPECT_THAT(record.mMarkedPosition.rot, ElementsAreArray(result.mMarkedPosition.rot)); + EXPECT_EQ(record.mCurrentCrimeId, result.mCurrentCrimeId); + EXPECT_EQ(record.mPaidCrimeId, result.mPaidCrimeId); + } + + TEST_P(Esm3SaveLoadRecordTest, cellRefShouldNotChange) + { + CellRef record; + record.blank(); + record.mRefNum.mIndex = std::numeric_limits::max(); + record.mRefNum.mContentFile = std::numeric_limits::max(); + record.mRefID = generateRandomRefId(); + record.mScale = 2; + record.mOwner = generateRandomRefId(); + record.mGlobalVariable = generateRandomString(100); + record.mSoul = generateRandomRefId(); + record.mFaction = generateRandomRefId(); + record.mFactionRank = std::numeric_limits::max(); + record.mChargeInt = std::numeric_limits::max(); + record.mEnchantmentCharge = std::numeric_limits::max(); + record.mCount = std::numeric_limits::max(); + record.mTeleport = true; + generateArray(record.mDoorDest.pos); + generateArray(record.mDoorDest.rot); + record.mDestCell = generateRandomString(100); + record.mLockLevel = 0; + record.mIsLocked = true; + record.mKey = generateRandomRefId(); + record.mTrap = generateRandomRefId(); + record.mReferenceBlocked = std::numeric_limits::max(); + generateArray(record.mPos.pos); + generateArray(record.mPos.rot); + CellRef result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(record.mRefNum.mIndex, result.mRefNum.mIndex); + EXPECT_EQ(record.mRefNum.mContentFile, result.mRefNum.mContentFile); + EXPECT_EQ(record.mRefID, result.mRefID); + EXPECT_EQ(record.mScale, result.mScale); + EXPECT_EQ(record.mOwner, result.mOwner); + EXPECT_EQ(record.mGlobalVariable, result.mGlobalVariable); + EXPECT_EQ(record.mSoul, result.mSoul); + EXPECT_EQ(record.mFaction, result.mFaction); + EXPECT_EQ(record.mFactionRank, result.mFactionRank); + EXPECT_EQ(record.mChargeInt, result.mChargeInt); + EXPECT_EQ(record.mEnchantmentCharge, result.mEnchantmentCharge); + EXPECT_EQ(record.mCount, result.mCount); + EXPECT_EQ(record.mTeleport, result.mTeleport); + EXPECT_EQ(record.mDoorDest, result.mDoorDest); + EXPECT_EQ(record.mDestCell, result.mDestCell); + EXPECT_EQ(record.mLockLevel, result.mLockLevel); + EXPECT_EQ(record.mIsLocked, result.mIsLocked); + EXPECT_EQ(record.mKey, result.mKey); + EXPECT_EQ(record.mTrap, result.mTrap); + EXPECT_EQ(record.mReferenceBlocked, result.mReferenceBlocked); + EXPECT_EQ(record.mPos, result.mPos); + } + + TEST_P(Esm3SaveLoadRecordTest, creatureStatsShouldNotChange) + { + CreatureStats record; + record.blank(); + record.mLastHitAttemptObject = generateRandomRefId(); + record.mLastHitObject = generateRandomRefId(); + CreatureStats result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(record.mLastHitAttemptObject, result.mLastHitAttemptObject); + EXPECT_EQ(record.mLastHitObject, result.mLastHitObject); + } + + TEST_P(Esm3SaveLoadRecordTest, containerShouldNotChange) + { + Container record; + record.blank(); + record.mId = generateRandomRefId(); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 42, .mItem = generateRandomRefId(32) }); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 13, .mItem = generateRandomRefId(32) }); + Container result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mInventory.mList, record.mInventory.mList); + } + + TEST_P(Esm3SaveLoadRecordTest, regionShouldNotChange) + { + Region record; + record.blank(); + record.mId = generateRandomRefId(); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(32), .mChance = 42 }); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(32), .mChance = 13 }); + Region result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mSoundList, record.mSoundList); + } + + TEST_P(Esm3SaveLoadRecordTest, scriptShouldNotChange) + { + Script record; + record.blank(); + record.mId = generateRandomRefId(32); + record.mNumShorts = 3; + record.mNumFloats = 4; + record.mNumLongs = 5; + generateStrings( + std::back_inserter(record.mVarNames), record.mNumShorts + record.mNumFloats + record.mNumLongs); + generateBytes(std::back_inserter(record.mScriptData), 13); + record.mScriptText = generateRandomString(17); + + Script result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mNumShorts, record.mNumShorts); + EXPECT_EQ(result.mNumFloats, record.mNumFloats); + EXPECT_EQ(result.mNumShorts, record.mNumShorts); + EXPECT_EQ(result.mVarNames, record.mVarNames); + EXPECT_EQ(result.mScriptData, record.mScriptData); + EXPECT_EQ(result.mScriptText, record.mScriptText); + } + + TEST_P(Esm3SaveLoadRecordTest, quickKeysShouldNotChange) + { + const QuickKeys record { + .mKeys = { + { + .mType = QuickKeys::Type::Magic, + .mId = generateRandomRefId(32), + }, + { + .mType = QuickKeys::Type::MagicItem, + .mId = generateRandomRefId(32), + }, + }, + }; + QuickKeys result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mKeys, record.mKeys); + } + + TEST_P(Esm3SaveLoadRecordTest, dialogueShouldNotChange) + { + Dialogue record; + record.blank(); + record.mStringId = generateRandomString(32); + record.mId = ESM::RefId::stringRefId(record.mStringId); + Dialogue result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mStringId, record.mStringId); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiWanderShouldNotChange) + { + AiSequence::AiWander record; + record.mData.mDistance = 1; + record.mData.mDuration = 2; + record.mData.mTimeOfDay = 3; + constexpr std::uint8_t idle[8] = { 4, 5, 6, 7, 8, 9, 10, 11 }; + static_assert(std::size(idle) == std::size(record.mData.mIdle)); + std::copy(std::begin(idle), std::end(idle), record.mData.mIdle); + record.mData.mShouldRepeat = 12; + record.mDurationData.mRemainingDuration = 13; + record.mStoredInitialActorPosition = true; + constexpr float initialActorPosition[3] = { 15, 16, 17 }; + static_assert(std::size(initialActorPosition) == std::size(record.mInitialActorPosition.mValues)); + std::copy( + std::begin(initialActorPosition), std::end(initialActorPosition), record.mInitialActorPosition.mValues); + + AiSequence::AiWander result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mDistance, record.mData.mDistance); + EXPECT_EQ(result.mData.mDuration, record.mData.mDuration); + EXPECT_EQ(result.mData.mTimeOfDay, record.mData.mTimeOfDay); + EXPECT_THAT(result.mData.mIdle, ElementsAreArray(record.mData.mIdle)); + EXPECT_EQ(result.mData.mShouldRepeat, record.mData.mShouldRepeat); + EXPECT_EQ(result.mDurationData.mRemainingDuration, record.mDurationData.mRemainingDuration); + EXPECT_EQ(result.mStoredInitialActorPosition, record.mStoredInitialActorPosition); + EXPECT_THAT(result.mInitialActorPosition.mValues, ElementsAreArray(record.mInitialActorPosition.mValues)); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiTravelShouldNotChange) + { + AiSequence::AiTravel record; + record.mData.mX = 1; + record.mData.mY = 2; + record.mData.mZ = 3; + record.mHidden = true; + record.mRepeat = true; + + AiSequence::AiTravel result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mX, record.mData.mX); + EXPECT_EQ(result.mData.mY, record.mData.mY); + EXPECT_EQ(result.mData.mZ, record.mData.mZ); + EXPECT_EQ(result.mHidden, record.mHidden); + EXPECT_EQ(result.mRepeat, record.mRepeat); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiEscortShouldNotChange) + { + AiSequence::AiEscort record; + record.mData.mX = 1; + record.mData.mY = 2; + record.mData.mZ = 3; + record.mData.mDuration = 4; + record.mTargetActorId = 5; + record.mTargetId = generateRandomRefId(32); + record.mCellId = generateRandomString(257); + record.mRemainingDuration = 6; + record.mRepeat = true; + + AiSequence::AiEscort result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mX, record.mData.mX); + EXPECT_EQ(result.mData.mY, record.mData.mY); + EXPECT_EQ(result.mData.mZ, record.mData.mZ); + if (GetParam() <= MaxOldAiPackageFormatVersion) + EXPECT_EQ(result.mData.mDuration, record.mRemainingDuration); + else + EXPECT_EQ(result.mData.mDuration, record.mData.mDuration); + EXPECT_EQ(result.mTargetActorId, record.mTargetActorId); + EXPECT_EQ(result.mTargetId, record.mTargetId); + EXPECT_EQ(result.mCellId, record.mCellId); + EXPECT_EQ(result.mRemainingDuration, record.mRemainingDuration); + EXPECT_EQ(result.mRepeat, record.mRepeat); + } + + TEST_P(Esm3SaveLoadRecordTest, aiDataShouldNotChange) + { + AIData record = { + .mHello = 1, + .mFight = 2, + .mFlee = 3, + .mAlarm = 4, + .mServices = 5, + }; + + AIData result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mHello, record.mHello); + EXPECT_EQ(result.mFight, record.mFight); + EXPECT_EQ(result.mFlee, record.mFlee); + EXPECT_EQ(result.mAlarm, record.mAlarm); + EXPECT_EQ(result.mServices, record.mServices); + } + + TEST_P(Esm3SaveLoadRecordTest, enamShouldNotChange) + { + EffectList record; + record.mList.emplace_back(IndexedENAMstruct{ { + .mEffectID = 1, + .mSkill = 2, + .mAttribute = 3, + .mRange = 4, + .mArea = 5, + .mDuration = 6, + .mMagnMin = 7, + .mMagnMax = 8, + }, + 0 }); + + EffectList result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mList.size(), record.mList.size()); + EXPECT_EQ(result.mList[0].mData.mEffectID, record.mList[0].mData.mEffectID); + EXPECT_EQ(result.mList[0].mData.mSkill, record.mList[0].mData.mSkill); + EXPECT_EQ(result.mList[0].mData.mAttribute, record.mList[0].mData.mAttribute); + EXPECT_EQ(result.mList[0].mData.mRange, record.mList[0].mData.mRange); + EXPECT_EQ(result.mList[0].mData.mArea, record.mList[0].mData.mArea); + EXPECT_EQ(result.mList[0].mData.mDuration, record.mList[0].mData.mDuration); + EXPECT_EQ(result.mList[0].mData.mMagnMin, record.mList[0].mData.mMagnMin); + EXPECT_EQ(result.mList[0].mData.mMagnMax, record.mList[0].mData.mMagnMax); + } + + TEST_P(Esm3SaveLoadRecordTest, weaponShouldNotChange) + { + Weapon record = { + .mData = { + .mWeight = 0, + .mValue = 1, + .mType = 2, + .mHealth = 3, + .mSpeed = 4, + .mReach = 5, + .mEnchant = 6, + .mChop = { 7, 8 }, + .mSlash = { 9, 10 }, + .mThrust = { 11, 12 }, + .mFlags = 13, + }, + .mRecordFlags = 0, + .mId = generateRandomRefId(32), + .mEnchant = generateRandomRefId(32), + .mScript = generateRandomRefId(32), + .mName = generateRandomString(32), + .mModel = generateRandomString(32), + .mIcon = generateRandomString(32), + }; + + Weapon result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mWeight, record.mData.mWeight); + EXPECT_EQ(result.mData.mValue, record.mData.mValue); + EXPECT_EQ(result.mData.mType, record.mData.mType); + EXPECT_EQ(result.mData.mHealth, record.mData.mHealth); + EXPECT_EQ(result.mData.mSpeed, record.mData.mSpeed); + EXPECT_EQ(result.mData.mReach, record.mData.mReach); + EXPECT_EQ(result.mData.mEnchant, record.mData.mEnchant); + EXPECT_EQ(result.mData.mChop, record.mData.mChop); + EXPECT_EQ(result.mData.mSlash, record.mData.mSlash); + EXPECT_EQ(result.mData.mThrust, record.mData.mThrust); + EXPECT_EQ(result.mData.mFlags, record.mData.mFlags); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mEnchant, record.mEnchant); + EXPECT_EQ(result.mScript, record.mScript); + EXPECT_EQ(result.mName, record.mName); + EXPECT_EQ(result.mModel, record.mModel); + EXPECT_EQ(result.mIcon, record.mIcon); + } + + TEST_P(Esm3SaveLoadRecordTest, infoShouldNotChange) + { + DialInfo record = { + .mData = { + .mType = ESM::Dialogue::Topic, + .mDisposition = 1, + .mRank = 2, + .mGender = ESM::DialInfo::NA, + .mPCrank = 3, + }, + .mSelects = { + ESM::DialogueCondition{ + .mVariable = {}, + .mValue = 42, + .mIndex = 0, + .mFunction = ESM::DialogueCondition::Function_Level, + .mComparison = ESM::DialogueCondition::Comp_Eq + }, + ESM::DialogueCondition{ + .mVariable = generateRandomString(32), + .mValue = 0, + .mIndex = 1, + .mFunction = ESM::DialogueCondition::Function_NotLocal, + .mComparison = ESM::DialogueCondition::Comp_Eq + }, + }, + .mId = generateRandomRefId(32), + .mPrev = generateRandomRefId(32), + .mNext = generateRandomRefId(32), + .mActor = generateRandomRefId(32), + .mRace = generateRandomRefId(32), + .mClass = generateRandomRefId(32), + .mFaction = generateRandomRefId(32), + .mPcFaction = generateRandomRefId(32), + .mCell = generateRandomRefId(32), + .mSound = generateRandomString(32), + .mResponse = generateRandomString(32), + .mResultScript = generateRandomString(32), + .mFactionLess = false, + .mQuestStatus = ESM::DialInfo::QS_None, + }; + + DialInfo result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mType, record.mData.mType); + EXPECT_EQ(result.mData.mDisposition, record.mData.mDisposition); + EXPECT_EQ(result.mData.mRank, record.mData.mRank); + EXPECT_EQ(result.mData.mGender, record.mData.mGender); + EXPECT_EQ(result.mData.mPCrank, record.mData.mPCrank); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mPrev, record.mPrev); + EXPECT_EQ(result.mNext, record.mNext); + EXPECT_EQ(result.mActor, record.mActor); + EXPECT_EQ(result.mRace, record.mRace); + EXPECT_EQ(result.mClass, record.mClass); + EXPECT_EQ(result.mFaction, record.mFaction); + EXPECT_EQ(result.mPcFaction, record.mPcFaction); + EXPECT_EQ(result.mCell, record.mCell); + EXPECT_EQ(result.mSound, record.mSound); + EXPECT_EQ(result.mResponse, record.mResponse); + EXPECT_EQ(result.mResultScript, record.mResultScript); + EXPECT_EQ(result.mFactionLess, record.mFactionLess); + EXPECT_EQ(result.mQuestStatus, record.mQuestStatus); + EXPECT_EQ(result.mSelects.size(), record.mSelects.size()); + for (size_t i = 0; i < result.mSelects.size(); ++i) + { + const auto& resultS = result.mSelects[i]; + const auto& recordS = record.mSelects[i]; + EXPECT_EQ(resultS.mVariable, recordS.mVariable); + EXPECT_EQ(resultS.mValue, recordS.mValue); + EXPECT_EQ(resultS.mIndex, recordS.mIndex); + EXPECT_EQ(resultS.mFunction, recordS.mFunction); + EXPECT_EQ(resultS.mComparison, recordS.mComparison); + } + } + + TEST_P(Esm3SaveLoadRecordTest, landShouldNotChange) + { + LandRecordData data; + std::iota(data.mHeights.begin(), data.mHeights.end(), 1); + std::for_each(data.mHeights.begin(), data.mHeights.end(), [](float& v) { v *= Land::sHeightScale; }); + data.mMinHeight = *std::min_element(data.mHeights.begin(), data.mHeights.end()); + data.mMaxHeight = *std::max_element(data.mHeights.begin(), data.mHeights.end()); + std::iota(data.mNormals.begin(), data.mNormals.end(), 2); + std::iota(data.mTextures.begin(), data.mTextures.end(), 3); + std::iota(data.mColours.begin(), data.mColours.end(), 4); + data.mDataLoaded = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_VCLR | Land::DATA_VTEX; + + Land record; + record.mFlags = Land::Flag_HeightsNormals | Land::Flag_Colors | Land::Flag_Textures; + record.mX = 2; + record.mY = 3; + record.mDataTypes = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_WNAM | Land::DATA_VCLR | Land::DATA_VTEX; + generateWnam(data.mHeights, record.mWnam); + record.mLandData = std::make_unique(data); + + Land result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mFlags, record.mFlags); + EXPECT_EQ(result.mX, record.mX); + EXPECT_EQ(result.mY, record.mY); + EXPECT_EQ(result.mDataTypes, record.mDataTypes); + EXPECT_EQ(result.mWnam, record.mWnam); + EXPECT_EQ(result.mLandData->mHeights, record.mLandData->mHeights); + EXPECT_EQ(result.mLandData->mMinHeight, record.mLandData->mMinHeight); + EXPECT_EQ(result.mLandData->mMaxHeight, record.mLandData->mMaxHeight); + EXPECT_EQ(result.mLandData->mNormals, record.mLandData->mNormals); + EXPECT_EQ(result.mLandData->mTextures, record.mLandData->mTextures); + EXPECT_EQ(result.mLandData->mColours, record.mLandData->mColours); + EXPECT_EQ(result.mLandData->mDataLoaded, record.mLandData->mDataLoaded); + } + + INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(getFormats())); + } +} diff --git a/apps/openmw_test_suite/esm4/includes.cpp b/apps/components_tests/esm4/includes.cpp similarity index 100% rename from apps/openmw_test_suite/esm4/includes.cpp rename to apps/components_tests/esm4/includes.cpp diff --git a/apps/openmw_test_suite/esmloader/esmdata.cpp b/apps/components_tests/esmloader/esmdata.cpp similarity index 100% rename from apps/openmw_test_suite/esmloader/esmdata.cpp rename to apps/components_tests/esmloader/esmdata.cpp diff --git a/apps/openmw_test_suite/esmloader/load.cpp b/apps/components_tests/esmloader/load.cpp similarity index 100% rename from apps/openmw_test_suite/esmloader/load.cpp rename to apps/components_tests/esmloader/load.cpp diff --git a/apps/openmw_test_suite/esmloader/record.cpp b/apps/components_tests/esmloader/record.cpp similarity index 100% rename from apps/openmw_test_suite/esmloader/record.cpp rename to apps/components_tests/esmloader/record.cpp diff --git a/apps/openmw_test_suite/esmterrain/testgridsampling.cpp b/apps/components_tests/esmterrain/testgridsampling.cpp similarity index 100% rename from apps/openmw_test_suite/esmterrain/testgridsampling.cpp rename to apps/components_tests/esmterrain/testgridsampling.cpp diff --git a/apps/openmw_test_suite/files/conversion_tests.cpp b/apps/components_tests/files/conversion_tests.cpp similarity index 100% rename from apps/openmw_test_suite/files/conversion_tests.cpp rename to apps/components_tests/files/conversion_tests.cpp diff --git a/apps/openmw_test_suite/files/hash.cpp b/apps/components_tests/files/hash.cpp similarity index 84% rename from apps/openmw_test_suite/files/hash.cpp rename to apps/components_tests/files/hash.cpp index 6ad19713dc..793965112b 100644 --- a/apps/openmw_test_suite/files/hash.cpp +++ b/apps/components_tests/files/hash.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include @@ -10,8 +12,6 @@ #include #include -#include "../testing_util.hpp" - namespace { using namespace testing; @@ -35,7 +35,8 @@ namespace std::fill_n(std::back_inserter(content), 1, 'a'); std::istringstream stream(content); stream.exceptions(std::ios::failbit | std::ios::badbit); - EXPECT_THAT(getHash(fileName, stream), ElementsAre(9607679276477937801ull, 16624257681780017498ull)); + EXPECT_THAT(getHash(Files::pathToUnicodeString(fileName), stream), + ElementsAre(9607679276477937801ull, 16624257681780017498ull)); } TEST_P(FilesGetHash, shouldReturnHashForStringStream) @@ -44,7 +45,7 @@ namespace std::string content; std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); std::istringstream stream(content); - EXPECT_EQ(getHash(fileName, stream), GetParam().mHash); + EXPECT_EQ(getHash(Files::pathToUnicodeString(fileName), stream), GetParam().mHash); } TEST_P(FilesGetHash, shouldReturnHashForConstrainedFileStream) @@ -57,7 +58,7 @@ namespace std::fstream(file, std::ios_base::out | std::ios_base::binary) .write(content.data(), static_cast(content.size())); const auto stream = Files::openConstrainedFileStream(file, 0, content.size()); - EXPECT_EQ(getHash(file, *stream), GetParam().mHash); + EXPECT_EQ(getHash(Files::pathToUnicodeString(file), *stream), GetParam().mHash); } INSTANTIATE_TEST_SUITE_P(Params, FilesGetHash, diff --git a/apps/openmw_test_suite/fx/lexer.cpp b/apps/components_tests/fx/lexer.cpp similarity index 100% rename from apps/openmw_test_suite/fx/lexer.cpp rename to apps/components_tests/fx/lexer.cpp diff --git a/apps/openmw_test_suite/fx/technique.cpp b/apps/components_tests/fx/technique.cpp similarity index 81% rename from apps/openmw_test_suite/fx/technique.cpp rename to apps/components_tests/fx/technique.cpp index e59cca43cf..ad57074b18 100644 --- a/apps/openmw_test_suite/fx/technique.cpp +++ b/apps/components_tests/fx/technique.cpp @@ -1,17 +1,17 @@ -#include "gmock/gmock.h" +#include #include #include #include #include #include - -#include "../testing_util.hpp" +#include namespace { + constexpr VFS::Path::NormalizedView techniquePropertiesPath("shaders/technique_properties.omwfx"); - TestingOpenMW::VFSTestFile technique_properties(R"( + TestingOpenMW::VFSTestFile techniqueProperties(R"( fragment main {} vertex main {} technique { @@ -27,7 +27,9 @@ namespace } )"); - TestingOpenMW::VFSTestFile rendertarget_properties{ R"( + constexpr VFS::Path::NormalizedView rendertargetPropertiesPath("shaders/rendertarget_properties.omwfx"); + + TestingOpenMW::VFSTestFile rendertargetProperties{ R"( render_target rendertarget { width_ratio = 0.5; height_ratio = 0.5; @@ -53,7 +55,9 @@ namespace technique { passes = downsample2x, main; } )" }; - TestingOpenMW::VFSTestFile uniform_properties{ R"( + constexpr VFS::Path::NormalizedView uniformPropertiesPath("shaders/uniform_properties.omwfx"); + + TestingOpenMW::VFSTestFile uniformProperties{ R"( uniform_vec4 uVec4 { default = vec4(0,0,0,0); min = vec4(0,1,0,0); @@ -67,13 +71,17 @@ namespace technique { passes = main; } )" }; - TestingOpenMW::VFSTestFile missing_sampler_source{ R"( + constexpr VFS::Path::NormalizedView missingSamplerSourcePath("shaders/missing_sampler_source.omwfx"); + + TestingOpenMW::VFSTestFile missingSamplerSource{ R"( sampler_1d mysampler1d { } fragment main { } technique { passes = main; } )" }; - TestingOpenMW::VFSTestFile repeated_shared_block{ R"( + constexpr VFS::Path::NormalizedView repeatedSharedBlockPath("shaders/repeated_shared_block.omwfx"); + + TestingOpenMW::VFSTestFile repeatedSharedBlock{ R"( shared { float myfloat = 1.0; } @@ -93,13 +101,13 @@ namespace TechniqueTest() : mVFS(TestingOpenMW::createTestVFS({ - { "shaders/technique_properties.omwfx", &technique_properties }, - { "shaders/rendertarget_properties.omwfx", &rendertarget_properties }, - { "shaders/uniform_properties.omwfx", &uniform_properties }, - { "shaders/missing_sampler_source.omwfx", &missing_sampler_source }, - { "shaders/repeated_shared_block.omwfx", &repeated_shared_block }, + { techniquePropertiesPath, &techniqueProperties }, + { rendertargetPropertiesPath, &rendertargetProperties }, + { uniformPropertiesPath, &uniformProperties }, + { missingSamplerSourcePath, &missingSamplerSource }, + { repeatedSharedBlockPath, &repeatedSharedBlock }, })) - , mImageManager(mVFS.get()) + , mImageManager(mVFS.get(), 0) { } diff --git a/apps/components_tests/lua/test_async.cpp b/apps/components_tests/lua/test_async.cpp new file mode 100644 index 0000000000..46120ad88a --- /dev/null +++ b/apps/components_tests/lua/test_async.cpp @@ -0,0 +1,57 @@ +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + + struct LuaCoroutineCallbackTest : Test + { + void SetUp() override + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table hiddenData(view.sol(), sol::create); + hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; + view.sol()["async"] = LuaUtil::getAsyncPackageInitializer( + view.sol(), []() { return 0.; }, []() { return 0.; })(hiddenData); + view.sol()["pass"] = [&](const sol::table& t) { mCb = LuaUtil::Callback::fromLua(t); }; + }); + } + + LuaUtil::LuaState mLua{ nullptr, nullptr }; + LuaUtil::Callback mCb; + }; + + TEST_F(LuaCoroutineCallbackTest, CoroutineCallbacks) + { + internal::CaptureStdout(); + mLua.protectedCall([&](LuaUtil::LuaView& view) { + view.sol().safe_script(R"X( + local s = 'test' + coroutine.wrap(function() + pass(async:callback(function(v) print(s) end)) + end)() + )X"); + view.sol().collect_garbage(); + mCb.call(); + }); + EXPECT_THAT(internal::GetCapturedStdout(), "test\n"); + } + + TEST_F(LuaCoroutineCallbackTest, ErrorInCoroutineCallbacks) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + view.sol().safe_script(R"X( + coroutine.wrap(function() + pass(async:callback(function() error('COROUTINE CALLBACK') end)) + end)() + )X"); + view.sol().collect_garbage(); + }); + EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK"); + } +} diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/components_tests/lua/test_configuration.cpp similarity index 94% rename from apps/openmw_test_suite/lua/test_configuration.cpp rename to apps/components_tests/lua/test_configuration.cpp index 49fc93a3d8..2cde0309d6 100644 --- a/apps/openmw_test_suite/lua/test_configuration.cpp +++ b/apps/components_tests/lua/test_configuration.cpp @@ -1,4 +1,4 @@ -#include "gmock/gmock.h" +#include #include #include @@ -9,8 +9,8 @@ #include #include #include - -#include "../testing_util.hpp" +#include +#include namespace { @@ -44,7 +44,7 @@ namespace EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "PLAYER NPC CREATURE : my_mod/some_other_script.lua"); - EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CUSTOM CREATURE : my_mod/creature.lua"); LuaUtil::ScriptsConfiguration conf; @@ -54,7 +54,7 @@ namespace // cfg.mScripts[1] is overridden by cfg.mScripts[4] // cfg.mScripts[2] is overridden by cfg.mScripts[3] EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "PLAYER NPC CREATURE : my_mod/some_other_script.lua"); - EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), ": my_mod/player.LUA"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), ": my_mod/player.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(conf[3]), "CUSTOM CREATURE : my_mod/creature.lua"); EXPECT_THAT(asVector(conf.getGlobalConf()), ElementsAre(Pair(0, ""))); @@ -89,7 +89,7 @@ namespace { ESM::LuaScriptsCfg cfg; ESM::LuaScriptCfg& script1 = cfg.mScripts.emplace_back(); - script1.mScriptPath = "Script1.lua"; + script1.mScriptPath = VFS::Path::Normalized("Script1.lua"); script1.mInitializationData = "data1"; script1.mFlags = ESM::LuaScriptCfg::sPlayer; script1.mTypes.push_back(ESM::REC_CREA); @@ -98,12 +98,12 @@ namespace script1.mRefs.push_back({ true, 2, 4, "" }); ESM::LuaScriptCfg& script2 = cfg.mScripts.emplace_back(); - script2.mScriptPath = "Script2.lua"; + script2.mScriptPath = VFS::Path::Normalized("Script2.lua"); script2.mFlags = ESM::LuaScriptCfg::sCustom; script2.mTypes.push_back(ESM::REC_CONT); ESM::LuaScriptCfg& script1Extra = cfg.mScripts.emplace_back(); - script1Extra.mScriptPath = "script1.LUA"; + script1Extra.mScriptPath = VFS::Path::Normalized("script1.LUA"); script1Extra.mFlags = ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sMerge; script1Extra.mTypes.push_back(ESM::REC_NPC_); script1Extra.mRecords.push_back({ false, ESM::RefId::stringRefId("rat"), "" }); @@ -115,8 +115,8 @@ namespace conf.init(cfg); ASSERT_EQ(conf.size(), 2); EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), - "CUSTOM PLAYER CREATURE NPC : Script1.lua ; data 5 bytes ; 3 records ; 4 objects"); - EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CUSTOM CONTAINER : Script2.lua"); + "CUSTOM PLAYER CREATURE NPC : script1.lua ; data 5 bytes ; 3 records ; 4 objects"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CUSTOM CONTAINER : script2.lua"); EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(0, "data1"))); EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, ESM::RefId::stringRefId("something"), ESM::RefNum())), @@ -139,7 +139,7 @@ namespace ElementsAre(Pair(0, "data1"), Pair(1, ""))); ESM::LuaScriptCfg& script3 = cfg.mScripts.emplace_back(); - script3.mScriptPath = "script1.lua"; + script3.mScriptPath = VFS::Path::Normalized("script1.lua"); script3.mFlags = ESM::LuaScriptCfg::sGlobal; EXPECT_ERROR(conf.init(cfg), "Flags mismatch for script1.lua"); } @@ -168,13 +168,13 @@ namespace } { ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); - script.mScriptPath = "test_global.lua"; + script.mScriptPath = VFS::Path::Normalized("test_global.lua"); script.mFlags = ESM::LuaScriptCfg::sGlobal; script.mInitializationData = luaData; } { ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); - script.mScriptPath = "test_local.lua"; + script.mScriptPath = VFS::Path::Normalized("test_local.lua"); script.mFlags = ESM::LuaScriptCfg::sMerge; script.mTypes.push_back(ESM::REC_DOOR); script.mTypes.push_back(ESM::REC_MISC); diff --git a/apps/components_tests/lua/test_inputactions.cpp b/apps/components_tests/lua/test_inputactions.cpp new file mode 100644 index 0000000000..cad17a5b99 --- /dev/null +++ b/apps/components_tests/lua/test_inputactions.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + TEST(LuaInputActionsTest, MultiTree) + { + { + LuaUtil::InputAction::MultiTree tree; + auto a = tree.insert(); + auto b = tree.insert(); + auto c = tree.insert(); + auto d = tree.insert(); + EXPECT_TRUE(tree.multiEdge(c, { a, b })); + EXPECT_TRUE(tree.multiEdge(a, { d })); + EXPECT_FALSE(tree.multiEdge(d, { c })); + } + + { + LuaUtil::InputAction::MultiTree tree; + auto a = tree.insert(); + auto b = tree.insert(); + auto c = tree.insert(); + EXPECT_TRUE(tree.multiEdge(b, { a })); + EXPECT_TRUE(tree.multiEdge(c, { a, b })); + } + } + + TEST(LuaInputActionsTest, Registry) + { + sol::state lua; + LuaUtil::InputAction::Registry registry; + LuaUtil::InputAction::Info a({ "a", LuaUtil::InputAction::Type::Boolean, "test", "a_name", "a_description", + sol::make_object(lua, false) }); + registry.insert(a); + LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description", + sol::make_object(lua, false) }); + registry.insert(b); + LuaUtil::Callback bindA({ lua.load("return function() return true end")(), sol::table(lua, sol::create) }); + LuaUtil::Callback bindBToA( + { lua.load("return function(_, _, aValue) return aValue end")(), sol::table(lua, sol::create) }); + EXPECT_TRUE(registry.bind("a", bindA, {})); + EXPECT_TRUE(registry.bind("b", bindBToA, { "a" })); + registry.update(1.0); + sol::object bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(bValue.is()); + LuaUtil::Callback badA( + { lua.load("return function() return 'not_a_bool' end")(), sol::table(lua, sol::create) }); + EXPECT_TRUE(registry.bind("a", badA, {})); + testing::internal::CaptureStderr(); + registry.update(1.0); + sol::object aValue = registry.valueOfType("a", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(aValue.is()); + bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(bValue.is() && bValue.as() == aValue.as()); + } +} diff --git a/apps/components_tests/lua/test_l10n.cpp b/apps/components_tests/lua/test_l10n.cpp new file mode 100644 index 0000000000..b48028a730 --- /dev/null +++ b/apps/components_tests/lua/test_l10n.cpp @@ -0,0 +1,174 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + template + T get(sol::state_view& lua, const std::string& luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + constexpr VFS::Path::NormalizedView test1EnPath("l10n/test1/en.yaml"); + constexpr VFS::Path::NormalizedView test1EnUsPath("l10n/test1/en_us.yaml"); + constexpr VFS::Path::NormalizedView test1DePath("l10n/test1/de.yaml"); + constexpr VFS::Path::NormalizedView test2EnPath("l10n/test2/en.yaml"); + constexpr VFS::Path::NormalizedView test3EnPath("l10n/test3/en.yaml"); + constexpr VFS::Path::NormalizedView test3DePath("l10n/test3/de.yaml"); + + VFSTestFile invalidScript("not a script"); + VFSTestFile incorrectScript( + "return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + VFSTestFile emptyScript(""); + + VFSTestFile test1En(R"X( +good_morning: "Good morning." +you_have_arrows: |- + {count, plural, + =0{You have no arrows.} + one{You have one arrow.} + other{You have {count} arrows.} + } +pc_must_come: |- + {PCGender, select, + male {He is} + female {She is} + other {They are} + } coming with us. +quest_completion: "The quest is {done, number, percent} complete." +ordinal: "You came in {num, ordinal} place." +spellout: "There {num, plural, one{is {num, spellout} thing} other{are {num, spellout} things}}." +duration: "It took {num, duration}" +numbers: "{int} and {double, number, integer} are integers, but {double} is a double" +rounding: "{value, number, :: .00}" +)X"); + + VFSTestFile test1De(R"X( +good_morning: "Guten Morgen." +you_have_arrows: |- + {count, plural, + one{Du hast ein Pfeil.} + other{Du hast {count} Pfeile.} + } +"Hello {name}!": "Hallo {name}!" +)X"); + + VFSTestFile test1EnUS(R"X( +currency: "You have {money, number, currency}" +)X"); + + VFSTestFile test2En(R"X( +good_morning: "Morning!" +you_have_arrows: "Arrows count: {count}" +)X"); + + struct LuaL10nTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + { test1EnPath, &test1En }, + { test1EnUsPath, &test1EnUS }, + { test1DePath, &test1De }, + { test2EnPath, &test2En }, + { test3EnPath, &test1En }, + { test3DePath, &test1De }, + }); + + LuaUtil::ScriptsConfiguration mCfg; + }; + + TEST_F(LuaL10nTest, L10n) + { + LuaUtil::LuaState lua{ mVFS.get(), &mCfg }; + lua.protectedCall([&](LuaUtil::LuaView& view) { + sol::state_view& l = view.sol(); + internal::CaptureStdout(); + l10n::Manager l10nManager(mVFS.get()); + l10nManager.setPreferredLocales({ "de", "en" }); + EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: gmst de en\n"); + + l["l10n"] = LuaUtil::initL10nLoader(l, &l10nManager); + + internal::CaptureStdout(); + l.safe_script("t1 = l10n('Test1')"); + EXPECT_THAT(internal::GetCapturedStdout(), + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n"); + + internal::CaptureStdout(); + l.safe_script("t2 = l10n('Test2')"); + { + std::string output = internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Language file \"l10n/Test2/en.yaml\" is enabled")); + } + + EXPECT_EQ(get(l, "t1('good_morning')"), "Guten Morgen."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile."); + EXPECT_EQ(get(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!"); + EXPECT_EQ(get(l, "t2('good_morning')"), "Morning!"); + EXPECT_EQ(get(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3"); + + internal::CaptureStdout(); + l10nManager.setPreferredLocales({ "en", "de" }); + EXPECT_THAT(internal::GetCapturedStdout(), + "Preferred locales: gmst en de\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n" + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test2/en.yaml\" is enabled\n"); + + EXPECT_EQ(get(l, "t1('good_morning')"), "Good morning."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=1})"), "You have one arrow."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"male\"})"), "He is coming with us."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"female\"})"), "She is coming with us."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"blah\"})"), "They are coming with us."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"other\"})"), "They are coming with us."); + EXPECT_EQ(get(l, "t1('quest_completion', {done=0.1})"), "The quest is 10% complete."); + EXPECT_EQ(get(l, "t1('quest_completion', {done=1})"), "The quest is 100% complete."); + EXPECT_EQ(get(l, "t1('ordinal', {num=1})"), "You came in 1st place."); + EXPECT_EQ(get(l, "t1('ordinal', {num=100})"), "You came in 100th place."); + EXPECT_EQ(get(l, "t1('spellout', {num=1})"), "There is one thing."); + EXPECT_EQ(get(l, "t1('spellout', {num=100})"), "There are one hundred things."); + EXPECT_EQ(get(l, "t1('duration', {num=100})"), "It took 1:40"); + EXPECT_EQ(get(l, "t1('numbers', {int=123, double=123.456})"), + "123 and 123 are integers, but 123.456 is a double"); + EXPECT_EQ(get(l, "t1('rounding', {value=123.456789})"), "123.46"); + // Check that failed messages display the key instead of an empty string + EXPECT_EQ(get(l, "t1('{mismatched_braces')"), "{mismatched_braces"); + EXPECT_EQ(get(l, "t1('{unknown_arg}')"), "{unknown_arg}"); + EXPECT_EQ(get(l, "t1('{num, integer}', {num=1})"), "{num, integer}"); + // Doesn't give a valid currency symbol with `en`. Not that openmw is designed for real world currency. + l10nManager.setPreferredLocales({ "en-US", "de" }); + EXPECT_EQ(get(l, "t1('currency', {money=10000.10})"), "You have $10,000.10"); + // Note: Not defined in English localisation file, so we fall back to the German before falling back to the + // key + EXPECT_EQ(get(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!"); + EXPECT_EQ(get(l, "t2('good_morning')"), "Morning!"); + EXPECT_EQ(get(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3"); + + // Test that locales with variants and country codes fall back to more generic locales + internal::CaptureStdout(); + l10nManager.setPreferredLocales({ "en-GB-oed", "de" }); + EXPECT_THAT(internal::GetCapturedStdout(), + "Preferred locales: gmst en_GB_OED de\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n" + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test2/en.yaml\" is enabled\n"); + EXPECT_EQ(get(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3"); + + // Test setting fallback language + l.safe_script("t3 = l10n('Test3', 'de')"); + l10nManager.setPreferredLocales({ "en" }); + EXPECT_EQ(get(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!"); + }); + } +} diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/components_tests/lua/test_lua.cpp similarity index 59% rename from apps/openmw_test_suite/lua/test_lua.cpp rename to apps/components_tests/lua/test_lua.cpp index 90c987522d..5f1e11c435 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/components_tests/lua/test_lua.cpp @@ -1,14 +1,16 @@ -#include "gmock/gmock.h" +#include #include #include - -#include "../testing_util.hpp" +#include +#include namespace { using namespace testing; + constexpr VFS::Path::NormalizedView counterPath("aaa/counter.lua"); + TestingOpenMW::VFSTestFile counterFile(R"X( x = 42 return { @@ -17,8 +19,12 @@ return { } )X"); + constexpr VFS::Path::NormalizedView invalidPath("invalid.lua"); + TestingOpenMW::VFSTestFile invalidScriptFile("Invalid script"); + constexpr VFS::Path::NormalizedView testsPath("bbb/tests.lua"); + TestingOpenMW::VFSTestFile testsFile(R"X( return { -- should work @@ -51,6 +57,11 @@ return { } )X"); + constexpr VFS::Path::NormalizedView metaIndexErrorPath("metaindexerror.lua"); + + TestingOpenMW::VFSTestFile metaIndexErrorFile( + "return setmetatable({}, { __index = function(t, key) error('meta index error') end })"); + std::string genBigScript() { std::stringstream buf; @@ -63,14 +74,24 @@ return { return buf.str(); } + constexpr VFS::Path::NormalizedView bigPath("big.lua"); + TestingOpenMW::VFSTestFile bigScriptFile(genBigScript()); + + constexpr VFS::Path::NormalizedView requireBigPath("requirebig.lua"); + TestingOpenMW::VFSTestFile requireBigScriptFile("local x = require('big') ; return {x}"); struct LuaStateTest : Test { - std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { "aaa/counter.lua", &counterFile }, - { "bbb/tests.lua", &testsFile }, { "invalid.lua", &invalidScriptFile }, { "big.lua", &bigScriptFile }, - { "requireBig.lua", &requireBigScriptFile } }); + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ + { counterPath, &counterFile }, + { testsPath, &testsFile }, + { invalidPath, &invalidScriptFile }, + { bigPath, &bigScriptFile }, + { requireBigPath, &requireBigScriptFile }, + { metaIndexErrorPath, &metaIndexErrorFile }, + }); LuaUtil::ScriptsConfiguration mCfg; LuaUtil::LuaState mLua{ mVFS.get(), &mCfg }; @@ -78,13 +99,14 @@ return { TEST_F(LuaStateTest, Sandbox) { - sol::table script1 = mLua.runInNewSandbox("aaa/counter.lua"); + const VFS::Path::Normalized path(counterPath); + sol::table script1 = mLua.runInNewSandbox(path); EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 42); LuaUtil::call(script1["inc"], 3); EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); - sol::table script2 = mLua.runInNewSandbox("aaa/counter.lua"); + sol::table script2 = mLua.runInNewSandbox(path); EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 42); LuaUtil::call(script2["inc"], 1); EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 43); @@ -94,37 +116,39 @@ return { TEST_F(LuaStateTest, ToString) { - EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), 3.14)), "3.14"); - EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), true)), "true"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), 3.14)), "3.14"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), true)), "true"); EXPECT_EQ(LuaUtil::toString(sol::nil), "nil"); - EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), "something")), "\"something\""); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), "something")), "\"something\""); } TEST_F(LuaStateTest, Cast) { - EXPECT_EQ(LuaUtil::cast(sol::make_object(mLua.sol(), 3.14)), 3); - EXPECT_ERROR( - LuaUtil::cast(sol::make_object(mLua.sol(), "3.14")), "Value \"\"3.14\"\" can not be casted to int"); - EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.sol(), sol::nil)), + EXPECT_EQ(LuaUtil::cast(sol::make_object(mLua.unsafeState(), 3.14)), 3); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), "3.14")), + "Value \"\"3.14\"\" can not be casted to int"); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), sol::nil)), "Value \"nil\" can not be casted to string"); - EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.sol(), sol::nil)), + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), sol::nil)), "Value \"nil\" can not be casted to string"); - EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.sol(), sol::nil)), + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), sol::nil)), "Value \"nil\" can not be casted to sol::table"); - EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.sol(), "3.14")), + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), "3.14")), "Value \"\"3.14\"\" can not be casted to sol::function"); - EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.sol(), "3.14")), + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), "3.14")), "Value \"\"3.14\"\" can not be casted to sol::function"); } TEST_F(LuaStateTest, ErrorHandling) { - EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:"); + const VFS::Path::Normalized path("invalid.lua"); + EXPECT_ERROR(mLua.runInNewSandbox(path), "[string \"invalid.lua\"]:1:"); } TEST_F(LuaStateTest, CustomRequire) { - sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + const VFS::Path::Normalized path("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); EXPECT_FLOAT_EQ( LuaUtil::call(script["sin"], 1).get(), -LuaUtil::call(script["requireMathSin"], -1).get()); @@ -132,7 +156,7 @@ return { EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 43); EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 44); { - sol::table script2 = mLua.runInNewSandbox("bbb/tests.lua"); + sol::table script2 = mLua.runInNewSandbox(path); EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get(), 43); } EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 45); @@ -142,7 +166,8 @@ return { TEST_F(LuaStateTest, ReadOnly) { - sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + const VFS::Path::Normalized path("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); // rawset itself is allowed EXPECT_EQ(LuaUtil::call(script["callRawset"]).get(), 3); @@ -158,46 +183,51 @@ return { TEST_F(LuaStateTest, Print) { + const VFS::Path::Normalized path("bbb/tests.lua"); { - sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); testing::internal::CaptureStdout(); LuaUtil::call(script["print"], 1, 2, 3); std::string output = testing::internal::GetCapturedStdout(); - EXPECT_EQ(output, "[bbb/tests.lua]:\t1\t2\t3\n"); + EXPECT_EQ(output, "unnamed:\t1\t2\t3\n"); } { - sol::table script = mLua.runInNewSandbox("bbb/tests.lua", "prefix"); + sol::table script = mLua.runInNewSandbox(path, "prefix"); testing::internal::CaptureStdout(); LuaUtil::call(script["print"]); // print with no arguments std::string output = testing::internal::GetCapturedStdout(); - EXPECT_EQ(output, "prefix[bbb/tests.lua]:\n"); + EXPECT_EQ(output, "prefix:\n"); } } TEST_F(LuaStateTest, UnsafeFunction) { - sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + const VFS::Path::Normalized path("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); EXPECT_ERROR(LuaUtil::call(script["callLoadstring"]), "a nil value"); } TEST_F(LuaStateTest, ProvideAPI) { LuaUtil::LuaState lua(mVFS.get(), &mCfg); + lua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table api1 = LuaUtil::makeReadOnly(view.sol().create_table_with("name", "api1")); + sol::table api2 = LuaUtil::makeReadOnly(view.sol().create_table_with("name", "api2")); - sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); - sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); + const VFS::Path::Normalized path("bbb/tests.lua"); - sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api1 } }); + sol::table script1 = lua.runInNewSandbox(path, "", { { "test.api", api1 } }); - lua.addCommonPackage("sqrlib", lua.sol().create_table_with("sqr", [](int x) { return x * x; })); + lua.addCommonPackage("sqrlib", view.sol().create_table_with("sqr", [](int x) { return x * x; })); - sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api2 } }); + sol::table script2 = lua.runInNewSandbox(path, "", { { "test.api", api2 } }); - EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib"); - EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get(), 9); + EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib"); + EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get(), 9); - EXPECT_EQ(LuaUtil::call(script1["apiName"]).get(), "api1"); - EXPECT_EQ(LuaUtil::call(script2["apiName"]).get(), "api2"); + EXPECT_EQ(LuaUtil::call(script1["apiName"]).get(), "api1"); + EXPECT_EQ(LuaUtil::call(script2["apiName"]).get(), "api2"); + }); } TEST_F(LuaStateTest, GetLuaVersion) @@ -209,18 +239,27 @@ return { { auto getMem = [&] { for (int i = 0; i < 5; ++i) - lua_gc(mLua.sol(), LUA_GCCOLLECT, 0); + lua_gc(mLua.unsafeState(), LUA_GCCOLLECT, 0); return mLua.getTotalMemoryUsage(); }; int64_t memWithScript; + const VFS::Path::Normalized path("requireBig.lua"); { - sol::object s = mLua.runInNewSandbox("requireBig.lua"); + sol::object s = mLua.runInNewSandbox(path); memWithScript = getMem(); } for (int i = 0; i < 100; ++i) // run many times to make small memory leaks visible - mLua.runInNewSandbox("requireBig.lua"); + mLua.runInNewSandbox(path); int64_t memWithoutScript = getMem(); // At this moment all instances of the script should be garbage-collected. EXPECT_LT(memWithoutScript, memWithScript); } + + TEST_F(LuaStateTest, SafeIndexMetamethod) + { + const VFS::Path::Normalized path("metaIndexError.lua"); + sol::table t = mLua.runInNewSandbox(path); + // without safe get we crash here + EXPECT_ERROR(LuaUtil::safeGet(t, "any key"), "meta index error"); + } } diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/components_tests/lua/test_scriptscontainer.cpp similarity index 50% rename from apps/openmw_test_suite/lua/test_scriptscontainer.cpp rename to apps/components_tests/lua/test_scriptscontainer.cpp index dc99caefda..4f3cca1b87 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/components_tests/lua/test_scriptscontainer.cpp @@ -1,4 +1,4 @@ -#include "gmock/gmock.h" +#include #include #include @@ -6,19 +6,31 @@ #include #include #include +#include -#include "../testing_util.hpp" +#include namespace { using namespace testing; using namespace TestingOpenMW; + constexpr VFS::Path::NormalizedView invalidPath("invalid.lua"); + VFSTestFile invalidScript("not a script"); + + constexpr VFS::Path::NormalizedView incorrectPath("incorrect.lua"); + VFSTestFile incorrectScript( "return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + + constexpr VFS::Path::NormalizedView emptyPath("empty.lua"); + VFSTestFile emptyScript(""); + constexpr VFS::Path::NormalizedView test1Path("test1.lua"); + constexpr VFS::Path::NormalizedView test2Path("test2.lua"); + VFSTestFile testScript(R"X( return { engineHandlers = { @@ -33,6 +45,8 @@ return { } )X"); + constexpr VFS::Path::NormalizedView stopEventPath("stopevent.lua"); + VFSTestFile stopEventScript(R"X( return { eventHandlers = { @@ -44,6 +58,9 @@ return { } )X"); + constexpr VFS::Path::NormalizedView loadSave1Path("loadsave1.lua"); + constexpr VFS::Path::NormalizedView loadSave2Path("loadsave2.lua"); + VFSTestFile loadSaveScript(R"X( x = 0 y = 0 @@ -70,6 +87,8 @@ return { } )X"); + constexpr VFS::Path::NormalizedView testInterfacePath("testinterface.lua"); + VFSTestFile interfaceScript(R"X( return { interfaceName = "TestInterface", @@ -80,6 +99,8 @@ return { } )X"); + constexpr VFS::Path::NormalizedView overrideInterfacePath("overrideinterface.lua"); + VFSTestFile overrideInterfaceScript(R"X( local old = nil local interface = { @@ -104,6 +125,8 @@ return { } )X"); + constexpr VFS::Path::NormalizedView useInterfacePath("useinterface.lua"); + VFSTestFile useInterfaceScript(R"X( local interfaces = require('openmw.interfaces') return { @@ -113,22 +136,72 @@ return { end, }, } +)X"); + + constexpr VFS::Path::NormalizedView unloadPath("unload.lua"); + + VFSTestFile unloadScript(R"X( +x = 0 +y = 0 +z = 0 +return { + engineHandlers = { + onSave = function(state) + print('saving', x, y, z) + return {x = x, y = y} + end, + onLoad = function(state) + x, y = state.x, state.y + print('loaded', x, y, z) + end + }, + eventHandlers = { + Set = function(eventData) + x, y, z = eventData.x, eventData.y, eventData.z + end + } +} +)X"); + + constexpr VFS::Path::NormalizedView customDataPath("customdata.lua"); + + VFSTestFile customDataScript(R"X( +data = nil +return { + engineHandlers = { + onSave = function() + return data + end, + onLoad = function(state) + data = state + end, + onInit = function(state) + data = state + end + }, + eventHandlers = { + WakeUp = function() + end + } +} )X"); struct LuaScriptsContainerTest : Test { std::unique_ptr mVFS = createTestVFS({ - { "invalid.lua", &invalidScript }, - { "incorrect.lua", &incorrectScript }, - { "empty.lua", &emptyScript }, - { "test1.lua", &testScript }, - { "test2.lua", &testScript }, - { "stopEvent.lua", &stopEventScript }, - { "loadSave1.lua", &loadSaveScript }, - { "loadSave2.lua", &loadSaveScript }, - { "testInterface.lua", &interfaceScript }, - { "overrideInterface.lua", &overrideInterfaceScript }, - { "useInterface.lua", &useInterfaceScript }, + { invalidPath, &invalidScript }, + { incorrectPath, &incorrectScript }, + { emptyPath, &emptyScript }, + { test1Path, &testScript }, + { test2Path, &testScript }, + { stopEventPath, &stopEventScript }, + { loadSave1Path, &loadSaveScript }, + { loadSave2Path, &loadSaveScript }, + { testInterfacePath, &interfaceScript }, + { overrideInterfacePath, &overrideInterfaceScript }, + { useInterfacePath, &useInterfaceScript }, + { unloadPath, &unloadScript }, + { customDataPath, &customDataScript }, }); LuaUtil::ScriptsConfiguration mCfg; @@ -149,42 +222,54 @@ CUSTOM, NPC: loadSave2.lua CUSTOM, PLAYER: testInterface.lua CUSTOM, PLAYER: overrideInterface.lua CUSTOM, PLAYER: useInterface.lua +CUSTOM: unload.lua +CUSTOM: customdata.lua )X"); mCfg.init(std::move(cfg)); } + + int getId(VFS::Path::NormalizedView path) const + { + const std::optional id = mCfg.findId(path); + if (!id.has_value()) + throw std::invalid_argument("Script id is not found: " + std::string(path.value())); + return *id; + } }; - TEST_F(LuaScriptsContainerTest, VerifyStructure) + TEST_F(LuaScriptsContainerTest, addCustomScriptShouldNotStartInvalidScript) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - { - testing::internal::CaptureStdout(); - EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua"))); - std::string output = testing::internal::GetCapturedStdout(); - EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); - } - { - testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("incorrect.lua"))); - std::string output = testing::internal::GetCapturedStdout(); - EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); - EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); - } - { - testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); - EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present - EXPECT_EQ(internal::GetCapturedStdout(), ""); - } + testing::internal::CaptureStdout(); + EXPECT_FALSE(scripts.addCustomScript(getId(invalidPath))); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); + } + + TEST_F(LuaScriptsContainerTest, addCustomScriptShouldNotSuportScriptsWithInvalidHandlerAndSection) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(getId(incorrectPath))); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); + EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); + } + + TEST_F(LuaScriptsContainerTest, addCustomScriptShouldReturnFalseForDuplicates) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addCustomScript(getId(emptyPath))); + EXPECT_FALSE(scripts.addCustomScript(getId(emptyPath))); } TEST_F(LuaScriptsContainerTest, CallHandler) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); + EXPECT_TRUE(scripts.addCustomScript(getId(test1Path))); + EXPECT_TRUE(scripts.addCustomScript(getId(stopEventPath))); + EXPECT_TRUE(scripts.addCustomScript(getId(test2Path))); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" @@ -194,12 +279,14 @@ CUSTOM, PLAYER: useInterface.lua TEST_F(LuaScriptsContainerTest, CallEvent) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); - std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); - std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); + EXPECT_TRUE(scripts.addCustomScript(getId(test1Path))); + EXPECT_TRUE(scripts.addCustomScript(getId(stopEventPath))); + EXPECT_TRUE(scripts.addCustomScript(getId(test2Path))); + + sol::state_view sol = mLua.unsafeState(); + std::string X0 = LuaUtil::serialize(sol.create_table_with("x", 0.5)); + std::string X1 = LuaUtil::serialize(sol.create_table_with("x", 1.5)); { testing::internal::CaptureStdout(); @@ -211,7 +298,7 @@ CUSTOM, PLAYER: useInterface.lua scripts.receiveEvent("Event1", X1); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test2.lua]:\t event1 1.5\n" - "Test[stopEvent.lua]:\t event1 1.5\n" + "Test[stopevent.lua]:\t event1 1.5\n" "Test[test1.lua]:\t event1 1.5\n"); } { @@ -226,7 +313,7 @@ CUSTOM, PLAYER: useInterface.lua scripts.receiveEvent("Event1", X0); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test2.lua]:\t event1 0.5\n" - "Test[stopEvent.lua]:\t event1 0.5\n"); + "Test[stopevent.lua]:\t event1 0.5\n"); } { testing::internal::CaptureStdout(); @@ -240,10 +327,13 @@ CUSTOM, PLAYER: useInterface.lua TEST_F(LuaScriptsContainerTest, RemoveScript) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); - EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); - std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); + + EXPECT_TRUE(scripts.addCustomScript(getId(test1Path))); + EXPECT_TRUE(scripts.addCustomScript(getId(stopEventPath))); + EXPECT_TRUE(scripts.addCustomScript(getId(test2Path))); + + sol::state_view sol = mLua.unsafeState(); + std::string X = LuaUtil::serialize(sol.create_table_with("x", 0.5)); { testing::internal::CaptureStdout(); @@ -253,11 +343,11 @@ CUSTOM, PLAYER: useInterface.lua "Test[test1.lua]:\t update 1.5\n" "Test[test2.lua]:\t update 1.5\n" "Test[test2.lua]:\t event1 0.5\n" - "Test[stopEvent.lua]:\t event1 0.5\n"); + "Test[stopevent.lua]:\t event1 0.5\n"); } { testing::internal::CaptureStdout(); - int stopEventScriptId = *mCfg.findId("stopEvent.lua"); + const int stopEventScriptId = getId(stopEventPath); EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); scripts.removeScript(stopEventScriptId); EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); @@ -271,7 +361,7 @@ CUSTOM, PLAYER: useInterface.lua } { testing::internal::CaptureStdout(); - scripts.removeScript(*mCfg.findId("test1.lua")); + scripts.removeScript(getId(test1Path)); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -288,19 +378,19 @@ CUSTOM, PLAYER: useInterface.lua scripts.addAutoStartedScripts(); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), - "Test[overrideInterface.lua]:\toverride\n" - "Test[overrideInterface.lua]:\tinit\n" - "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" - "Test[testInterface.lua]:\tFN\t4.5\n"); + "Test[overrideinterface.lua]:\toverride\n" + "Test[overrideinterface.lua]:\tinit\n" + "Test[overrideinterface.lua]:\tNEW FN\t4.5\n" + "Test[testinterface.lua]:\tFN\t4.5\n"); } TEST_F(LuaScriptsContainerTest, Interface) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); scripts.setAutoStartConf(mCfg.getLocalConf(ESM::REC_CREA, ESM::RefId(), ESM::RefNum())); - int addIfaceId = *mCfg.findId("testInterface.lua"); - int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); - int useIfaceId = *mCfg.findId("useInterface.lua"); + const int addIfaceId = getId(testInterfacePath); + const int overrideIfaceId = getId(overrideInterfacePath); + const int useIfaceId = getId(useInterfacePath); testing::internal::CaptureStdout(); scripts.addAutoStartedScripts(); @@ -315,11 +405,11 @@ CUSTOM, PLAYER: useInterface.lua scripts.removeScript(overrideIfaceId); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), - "Test[overrideInterface.lua]:\toverride\n" - "Test[overrideInterface.lua]:\tinit\n" - "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" - "Test[testInterface.lua]:\tFN\t4.5\n" - "Test[testInterface.lua]:\tFN\t3.5\n"); + "Test[overrideinterface.lua]:\toverride\n" + "Test[overrideinterface.lua]:\tinit\n" + "Test[overrideinterface.lua]:\tNEW FN\t4.5\n" + "Test[testinterface.lua]:\tFN\t4.5\n" + "Test[testinterface.lua]:\tFN\t3.5\n"); } TEST_F(LuaScriptsContainerTest, LoadSave) @@ -332,10 +422,11 @@ CUSTOM, PLAYER: useInterface.lua scripts3.setAutoStartConf(mCfg.getPlayerConf()); scripts1.addAutoStartedScripts(); - EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts1.addCustomScript(getId(test1Path))); - scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with("n", 1, "x", 0.5, "y", 3.5))); - scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with("n", 2, "x", 2.5, "y", 1.5))); + sol::state_view sol = mLua.unsafeState(); + scripts1.receiveEvent("Set", LuaUtil::serialize(sol.create_table_with("n", 1, "x", 0.5, "y", 3.5))); + scripts1.receiveEvent("Set", LuaUtil::serialize(sol.create_table_with("n", 2, "x", 2.5, "y", 1.5))); ESM::LuaScripts data; scripts1.save(data); @@ -346,23 +437,23 @@ CUSTOM, PLAYER: useInterface.lua scripts2.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\n" - "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[loadSave1.lua]:\t2.5\t1.5\n" + "Test[loadsave2.lua]:\t0.5\t3.5\n" + "Test[loadsave1.lua]:\t2.5\t1.5\n" "Test[test1.lua]:\tprint\n"); - EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua"))); + EXPECT_FALSE(scripts2.hasScript(getId(testInterfacePath))); } { testing::internal::CaptureStdout(); scripts3.load(data); scripts3.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), - "Ignoring Test[loadSave1.lua]; this script is not allowed here\n" + "Ignoring Test[loadsave1.lua]; this script is not allowed here\n" "Test[test1.lua]:\tload\n" - "Test[overrideInterface.lua]:\toverride\n" - "Test[overrideInterface.lua]:\tinit\n" - "Test[loadSave2.lua]:\t0.5\t3.5\n" + "Test[overrideinterface.lua]:\toverride\n" + "Test[overrideinterface.lua]:\tinit\n" + "Test[loadsave2.lua]:\t0.5\t3.5\n" "Test[test1.lua]:\tprint\n"); - EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua"))); + EXPECT_TRUE(scripts3.hasScript(getId(testInterfacePath))); } } @@ -370,8 +461,8 @@ CUSTOM, PLAYER: useInterface.lua { using TimerType = LuaUtil::ScriptsContainer::TimerType; LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - int test1Id = *mCfg.findId("test1.lua"); - int test2Id = *mCfg.findId("test2.lua"); + const int test1Id = getId(test1Path); + const int test2Id = getId(test2Path); testing::internal::CaptureStdout(); EXPECT_TRUE(scripts.addCustomScript(test1Id)); @@ -379,10 +470,10 @@ CUSTOM, PLAYER: useInterface.lua EXPECT_EQ(internal::GetCapturedStdout(), ""); int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; - sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; }); - sol::function fn2 = sol::make_object(mLua.sol(), [&]() { counter2++; }); - sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; }); - sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; }); + sol::function fn1 = sol::make_object(mLua.unsafeState(), [&]() { counter1++; }); + sol::function fn2 = sol::make_object(mLua.unsafeState(), [&]() { counter2++; }); + sol::function fn3 = sol::make_object(mLua.unsafeState(), [&](int d) { counter3 += d; }); + sol::function fn4 = sol::make_object(mLua.unsafeState(), [&](int d) { counter4 += d; }); scripts.registerTimerCallback(test1Id, "A", fn3); scripts.registerTimerCallback(test1Id, "B", fn4); @@ -391,12 +482,16 @@ CUSTOM, PLAYER: useInterface.lua scripts.processTimers(1, 2); - scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); - scripts.setupSerializableTimer(TimerType::GAME_TIME, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); - scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); - scripts.setupSerializableTimer(TimerType::GAME_TIME, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); - scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); - scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 10, test1Id, "B", sol::make_object(mLua.unsafeState(), 3)); + scripts.setupSerializableTimer(TimerType::GAME_TIME, 10, test2Id, "B", sol::make_object(mLua.unsafeState(), 4)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 5, test1Id, "A", sol::make_object(mLua.unsafeState(), 1)); + scripts.setupSerializableTimer(TimerType::GAME_TIME, 5, test2Id, "A", sol::make_object(mLua.unsafeState(), 2)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 15, test1Id, "A", sol::make_object(mLua.unsafeState(), 10)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 15, test1Id, "B", sol::make_object(mLua.unsafeState(), 20)); scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 10, test2Id, fn2); scripts.setupUnsavableTimer(TimerType::GAME_TIME, 10, test1Id, fn2); @@ -446,7 +541,8 @@ CUSTOM, PLAYER: useInterface.lua TEST_F(LuaScriptsContainerTest, CallbackWrapper) { - LuaUtil::Callback callback{ mLua.sol()["print"], mLua.newTable() }; + sol::state_view view = mLua.unsafeState(); + LuaUtil::Callback callback{ view["print"], sol::table(view, sol::create) }; callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua"; callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{ nullptr, 0 }; @@ -458,10 +554,99 @@ CUSTOM, PLAYER: useInterface.lua callback.call(1.5, 2.5); EXPECT_EQ(internal::GetCapturedStdout(), "1.5\t2.5\n"); + const Debug::Level level = std::exchange(Log::sMinDebugLevel, Debug::All); + testing::internal::CaptureStdout(); callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = sol::nil; callback.call(1.5, 2.5); EXPECT_EQ(internal::GetCapturedStdout(), "Ignored callback to the removed script some_script.lua\n"); + + Log::sMinDebugLevel = level; } + TEST_F(LuaScriptsContainerTest, Unload) + { + LuaUtil::ScriptTracker tracker; + LuaUtil::ScriptsContainer scripts1(&mLua, "Test", &tracker, false); + + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId(unloadPath))); + EXPECT_EQ(tracker.size(), 1); + + mLua.protectedCall([&](LuaUtil::LuaView& lua) { + scripts1.receiveEvent("Set", LuaUtil::serialize(lua.sol().create_table_with("x", 3, "y", 2, "z", 1))); + testing::internal::CaptureStdout(); + for (int i = 0; i < 600; ++i) + tracker.unloadInactiveScripts(lua); + EXPECT_EQ(tracker.size(), 0); + scripts1.receiveEvent("Set", LuaUtil::serialize(lua.sol().create_table_with("x", 10, "y", 20, "z", 30))); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[unload.lua]:\tsaving\t3\t2\t1\n" + "Test[unload.lua]:\tloaded\t3\t2\t0\n"); + }); + EXPECT_EQ(tracker.size(), 1); + ESM::LuaScripts data; + scripts1.save(data); + EXPECT_EQ(tracker.size(), 1); + mLua.protectedCall([&](LuaUtil::LuaView& lua) { + for (int i = 0; i < 600; ++i) + tracker.unloadInactiveScripts(lua); + }); + EXPECT_EQ(tracker.size(), 0); + scripts1.load(data); + EXPECT_EQ(tracker.size(), 0); + } + + TEST_F(LuaScriptsContainerTest, LoadOrderChange) + { + LuaUtil::ScriptTracker tracker; + LuaUtil::ScriptsContainer scripts1(&mLua, "Test", &tracker, false); + LuaUtil::BasicSerializer serializer1; + LuaUtil::BasicSerializer serializer2([](int contentFileIndex) -> int { + if (contentFileIndex == 12) + return 34; + else if (contentFileIndex == 37) + return 12; + return contentFileIndex; + }); + scripts1.setSerializer(&serializer1); + scripts1.setSavedDataDeserializer(&serializer2); + + mLua.protectedCall([&](LuaUtil::LuaView& lua) { + sol::object id1 = sol::make_object_userdata(lua.sol(), ESM::RefNum{ 42, 12 }); + sol::object id2 = sol::make_object_userdata(lua.sol(), ESM::RefNum{ 13, 37 }); + sol::table table = lua.newTable(); + table[id1] = id2; + LuaUtil::BinaryData serialized = LuaUtil::serialize(table, &serializer1); + + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId(customDataPath), serialized)); + EXPECT_EQ(tracker.size(), 1); + for (int i = 0; i < 600; ++i) + tracker.unloadInactiveScripts(lua); + EXPECT_EQ(tracker.size(), 0); + scripts1.receiveEvent("WakeUp", {}); + EXPECT_EQ(tracker.size(), 1); + }); + + ESM::LuaScripts data1; + ESM::LuaScripts data2; + scripts1.save(data1); + scripts1.load(data1); + scripts1.save(data2); + EXPECT_NE(data1.mScripts[0].mData, data2.mScripts[0].mData); + + mLua.protectedCall([&](LuaUtil::LuaView& lua) { + sol::object deserialized = LuaUtil::deserialize(lua.sol(), data2.mScripts[0].mData, &serializer1); + EXPECT_TRUE(deserialized.is()); + sol::table table = deserialized; + for (const auto& [key, value] : table) + { + EXPECT_TRUE(key.is()); + EXPECT_TRUE(value.is()); + EXPECT_EQ(key.as(), (ESM::RefNum{ 42, 34 })); + EXPECT_EQ(value.as(), (ESM::RefNum{ 13, 12 })); + return; + } + EXPECT_FALSE(true); + }); + } } diff --git a/apps/openmw_test_suite/lua/test_serialization.cpp b/apps/components_tests/lua/test_serialization.cpp similarity index 99% rename from apps/openmw_test_suite/lua/test_serialization.cpp rename to apps/components_tests/lua/test_serialization.cpp index 56a6210b28..cff41dde9a 100644 --- a/apps/openmw_test_suite/lua/test_serialization.cpp +++ b/apps/components_tests/lua/test_serialization.cpp @@ -1,4 +1,4 @@ -#include "gmock/gmock.h" +#include #include #include @@ -13,7 +13,7 @@ #include #include -#include "../testing_util.hpp" +#include namespace { diff --git a/apps/components_tests/lua/test_storage.cpp b/apps/components_tests/lua/test_storage.cpp new file mode 100644 index 0000000000..cf6db0ca64 --- /dev/null +++ b/apps/components_tests/lua/test_storage.cpp @@ -0,0 +1,128 @@ +#include +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + + template + T get(sol::state_view& lua, std::string luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + TEST(LuaUtilStorageTest, Subscribe) + { + // Note: LuaUtil::Callback can be used only if Lua is initialized via LuaUtil::LuaState + LuaUtil::LuaState luaState{ nullptr, nullptr }; + luaState.protectedCall([](LuaUtil::LuaView& view) { + sol::state_view& lua = view.sol(); + LuaUtil::LuaStorage::initLuaBindings(view); + LuaUtil::LuaStorage storage; + storage.setActive(true); + + sol::table callbackHiddenData(lua, sol::create); + callbackHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; + LuaUtil::getAsyncPackageInitializer( + lua.lua_state(), []() { return 0.0; }, []() { return 0.0; })(callbackHiddenData); + lua["async"] = LuaUtil::AsyncPackageId{ nullptr, 0, callbackHiddenData }; + + lua["mutable"] = storage.getMutableSection(lua, "test"); + lua["ro"] = storage.getReadOnlySection(lua, "test"); + + lua.safe_script(R"( + callbackCalls = {} + ro:subscribe(async:callback(function(section, key) + table.insert(callbackCalls, section .. '_' .. (key or '*')) + end)) + )"); + + lua.safe_script("mutable:set('x', 5)"); + EXPECT_EQ(get(lua, "mutable:get('x')"), 5); + EXPECT_EQ(get(lua, "ro:get('x')"), 5); + + EXPECT_THROW(lua.safe_script("ro:set('y', 3)"), std::exception); + + lua.safe_script("t1 = mutable:asTable()"); + lua.safe_script("t2 = ro:asTable()"); + EXPECT_EQ(get(lua, "t1.x"), 5); + EXPECT_EQ(get(lua, "t2.x"), 5); + + lua.safe_script("mutable:reset()"); + EXPECT_TRUE(get(lua, "ro:get('x') == nil")); + + lua.safe_script("mutable:reset({x=4, y=7})"); + EXPECT_EQ(get(lua, "ro:get('x')"), 4); + EXPECT_EQ(get(lua, "ro:get('y')"), 7); + + EXPECT_THAT(get(lua, "table.concat(callbackCalls, ', ')"), "test_x, test_*, test_*"); + }); + } + + TEST(LuaUtilStorageTest, Table) + { + LuaUtil::LuaState luaState{ nullptr, nullptr }; + luaState.protectedCall([](LuaUtil::LuaView& view) { + LuaUtil::LuaStorage::initLuaBindings(view); + LuaUtil::LuaStorage storage; + auto& lua = view.sol(); + storage.setActive(true); + lua["mutable"] = storage.getMutableSection(lua, "test"); + lua["ro"] = storage.getReadOnlySection(lua, "test"); + + lua.safe_script("mutable:set('x', { y = 'abc', z = 7 })"); + EXPECT_EQ(get(lua, "mutable:get('x').z"), 7); + EXPECT_THROW(lua.safe_script("mutable:get('x').z = 3"), std::exception); + EXPECT_NO_THROW(lua.safe_script("mutable:getCopy('x').z = 3")); + EXPECT_EQ(get(lua, "mutable:get('x').z"), 7); + EXPECT_EQ(get(lua, "ro:get('x').z"), 7); + EXPECT_EQ(get(lua, "ro:get('x').y"), "abc"); + }); + } + + TEST(LuaUtilStorageTest, Saving) + { + LuaUtil::LuaState luaState{ nullptr, nullptr }; + luaState.protectedCall([](LuaUtil::LuaView& view) { + LuaUtil::LuaStorage::initLuaBindings(view); + LuaUtil::LuaStorage storage; + auto& lua = view.sol(); + storage.setActive(true); + + lua["permanent"] = storage.getMutableSection(lua, "permanent"); + lua["temporary"] = storage.getMutableSection(lua, "temporary"); + lua.safe_script("temporary:removeOnExit()"); + lua.safe_script("permanent:set('x', 1)"); + lua.safe_script("temporary:set('y', 2)"); + + const auto tmpFile = std::filesystem::temp_directory_path() / "test_storage.bin"; + storage.save(lua, tmpFile); + EXPECT_EQ(get(lua, "permanent:get('x')"), 1); + EXPECT_EQ(get(lua, "temporary:get('y')"), 2); + + storage.clearTemporaryAndRemoveCallbacks(); + lua["permanent"] = storage.getMutableSection(lua, "permanent"); + lua["temporary"] = storage.getMutableSection(lua, "temporary"); + EXPECT_EQ(get(lua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(lua, "temporary:get('y') == nil")); + + lua.safe_script("permanent:set('x', 3)"); + lua.safe_script("permanent:set('z', 4)"); + + LuaUtil::LuaStorage storage2; + storage2.setActive(true); + storage2.load(lua, tmpFile); + lua["permanent"] = storage2.getMutableSection(lua, "permanent"); + lua["temporary"] = storage2.getMutableSection(lua, "temporary"); + + EXPECT_EQ(get(lua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(lua, "permanent:get('z') == nil")); + EXPECT_TRUE(get(lua, "temporary:get('y') == nil")); + }); + } + +} diff --git a/apps/openmw_test_suite/lua/test_ui_content.cpp b/apps/components_tests/lua/test_ui_content.cpp similarity index 90% rename from apps/openmw_test_suite/lua/test_ui_content.cpp rename to apps/components_tests/lua/test_ui_content.cpp index 01a5cdd370..fcdfd8a1b3 100644 --- a/apps/openmw_test_suite/lua/test_ui_content.cpp +++ b/apps/components_tests/lua/test_ui_content.cpp @@ -14,7 +14,8 @@ namespace sol::protected_function mNew; LuaUiContentTest() { - mLuaState.addInternalLibSearchPath("resources/lua_libs"); + mLuaState.addInternalLibSearchPath( + std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "components" / "lua_ui"); mNew = LuaUi::loadContentConstructor(&mLuaState); } @@ -26,7 +27,7 @@ namespace return LuaUi::ContentView(result.get()); } - sol::table makeTable() { return sol::table(mLuaState.sol(), sol::create); } + sol::table makeTable() { return sol::table(mLuaState.unsafeState(), sol::create); } sol::table makeTable(std::string name) { @@ -38,13 +39,14 @@ namespace TEST_F(LuaUiContentTest, ProtectedMetatable) { - mLuaState.sol()["makeContent"] = mNew; - mLuaState.sol()["M"] = makeContent(makeTable()).getMetatable(); + sol::state_view sol = mLuaState.unsafeState(); + sol["makeContent"] = mNew; + sol["M"] = makeContent(makeTable()).getMetatable(); std::string testScript = R"( assert(not pcall(function() setmetatable(makeContent{}, {}) end), 'Metatable is not protected') assert(getmetatable(makeContent{}) == false, 'Metatable is not protected') )"; - EXPECT_NO_THROW(mLuaState.sol().safe_script(testScript)); + EXPECT_NO_THROW(sol.safe_script(testScript)); } TEST_F(LuaUiContentTest, Create) diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/components_tests/lua/test_utilpackage.cpp similarity index 88% rename from apps/openmw_test_suite/lua/test_utilpackage.cpp rename to apps/components_tests/lua/test_utilpackage.cpp index 26bdf3408b..a61c0e0306 100644 --- a/apps/openmw_test_suite/lua/test_utilpackage.cpp +++ b/apps/components_tests/lua/test_utilpackage.cpp @@ -1,10 +1,9 @@ -#include "gmock/gmock.h" +#include #include #include #include - -#include "../testing_util.hpp" +#include namespace { @@ -50,6 +49,10 @@ namespace EXPECT_TRUE(get(lua, "ediv0.x == math.huge and ediv0.y ~= ediv0.y")); EXPECT_TRUE(get(lua, "util.vector2(1, 2):emul(util.vector2(3, 4)) == util.vector2(3, 8)")); EXPECT_TRUE(get(lua, "util.vector2(4, 6):ediv(util.vector2(2, 3)) == util.vector2(2, 2)")); + lua.safe_script("swizzle = util.vector2(1, 2)"); + EXPECT_TRUE(get(lua, "swizzle.xx == util.vector2(1, 1) and swizzle.yy == util.vector2(2, 2)")); + EXPECT_TRUE(get(lua, "swizzle.y0 == util.vector2(2, 0) and swizzle.x1 == util.vector2(1, 1)")); + EXPECT_TRUE(get(lua, "swizzle['01'] == util.vector2(0, 1) and swizzle['0y'] == util.vector2(0, 2)")); } TEST(LuaUtilPackageTest, Vector3) @@ -83,6 +86,12 @@ namespace EXPECT_TRUE(get(lua, "ediv0.z == math.huge")); EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3):emul(util.vector3(3, 4, 5)) == util.vector3(3, 8, 15)")); EXPECT_TRUE(get(lua, "util.vector3(4, 6, 8):ediv(util.vector3(2, 3, 4)) == util.vector3(2, 2, 2)")); + lua.safe_script("swizzle = util.vector3(1, 2, 3)"); + EXPECT_TRUE(get(lua, "swizzle.xxx == util.vector3(1, 1, 1)")); + EXPECT_TRUE(get(lua, "swizzle.xyz == swizzle.zyx.zyx")); + EXPECT_TRUE(get(lua, "swizzle.xy0 == util.vector3(1, 2, 0) and swizzle.x11 == util.vector3(1, 1, 1)")); + EXPECT_TRUE( + get(lua, "swizzle['001'] == util.vector3(0, 0, 1) and swizzle['0yx'] == util.vector3(0, 2, 1)")); } TEST(LuaUtilPackageTest, Vector4) @@ -117,6 +126,14 @@ namespace get(lua, "util.vector4(1, 2, 3, 4):emul(util.vector4(3, 4, 5, 6)) == util.vector4(3, 8, 15, 24)")); EXPECT_TRUE( get(lua, "util.vector4(4, 6, 8, 9):ediv(util.vector4(2, 3, 4, 3)) == util.vector4(2, 2, 2, 3)")); + lua.safe_script("swizzle = util.vector4(1, 2, 3, 4)"); + EXPECT_TRUE(get(lua, "swizzle.wwww == util.vector4(4, 4, 4, 4)")); + EXPECT_TRUE(get(lua, "swizzle.xyzw == util.vector4(1, 2, 3, 4)")); + EXPECT_TRUE(get(lua, "swizzle.xyzw == swizzle.wzyx.wzyx")); + EXPECT_TRUE( + get(lua, "swizzle.xyz0 == util.vector4(1, 2, 3, 0) and swizzle.w110 == util.vector4(4, 1, 1, 0)")); + EXPECT_TRUE(get( + lua, "swizzle['0001'] == util.vector4(0, 0, 0, 1) and swizzle['0yx1'] == util.vector4(0, 2, 1, 1)")); } TEST(LuaUtilPackageTest, Color) diff --git a/apps/components_tests/lua/test_yaml.cpp b/apps/components_tests/lua/test_yaml.cpp new file mode 100644 index 0000000000..fa28889440 --- /dev/null +++ b/apps/components_tests/lua/test_yaml.cpp @@ -0,0 +1,357 @@ +#include + +#include +#include +#include + +#include + +#include + +namespace +{ + template + bool checkNumber(sol::state_view& lua, const std::string& inputData, T requiredValue) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::number) + return false; + + return result.as() == requiredValue; + } + + bool checkBool(sol::state_view& lua, const std::string& inputData, bool requiredValue) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::boolean) + return false; + + return result.as() == requiredValue; + } + + bool checkNil(sol::state_view& lua, const std::string& inputData) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + return result == sol::nil; + } + + bool checkNan(sol::state_view& lua, const std::string& inputData) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::number) + return false; + + return std::isnan(result.as()); + } + + bool checkString(sol::state_view& lua, const std::string& inputData, const std::string& requiredValue) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::string) + return false; + + return result.as() == requiredValue; + } + + bool checkString(sol::state_view& lua, const std::string& inputData) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::string) + return false; + + return result.as() == inputData; + } + + TEST(LuaUtilYamlLoader, ScalarTypeDeduction) + { + sol::state lua; + + ASSERT_TRUE(checkNil(lua, "null")); + ASSERT_TRUE(checkNil(lua, "Null")); + ASSERT_TRUE(checkNil(lua, "NULL")); + ASSERT_TRUE(checkNil(lua, "~")); + ASSERT_TRUE(checkNil(lua, "")); + ASSERT_FALSE(checkNil(lua, "NUll")); + ASSERT_TRUE(checkString(lua, "NUll")); + ASSERT_TRUE(checkString(lua, "'null'", "null")); + + ASSERT_TRUE(checkNumber(lua, "017", 17)); + ASSERT_TRUE(checkNumber(lua, "-017", -17)); + ASSERT_TRUE(checkNumber(lua, "+017", 17)); + ASSERT_TRUE(checkNumber(lua, "17", 17)); + ASSERT_TRUE(checkNumber(lua, "-17", -17)); + ASSERT_TRUE(checkNumber(lua, "+17", 17)); + ASSERT_TRUE(checkNumber(lua, "0o17", 15)); + ASSERT_TRUE(checkString(lua, "-0o17")); + ASSERT_TRUE(checkString(lua, "+0o17")); + ASSERT_TRUE(checkString(lua, "0b1")); + ASSERT_TRUE(checkString(lua, "1:00")); + ASSERT_TRUE(checkString(lua, "'17'", "17")); + ASSERT_TRUE(checkNumber(lua, "0x17", 23)); + ASSERT_TRUE(checkString(lua, "'-0x17'", "-0x17")); + ASSERT_TRUE(checkString(lua, "'+0x17'", "+0x17")); + + ASSERT_TRUE(checkNumber(lua, "2.1e-05", 2.1e-5)); + ASSERT_TRUE(checkNumber(lua, "-2.1e-05", -2.1e-5)); + ASSERT_TRUE(checkNumber(lua, "+2.1e-05", 2.1e-5)); + ASSERT_TRUE(checkNumber(lua, "2.1e+5", 210000)); + ASSERT_TRUE(checkNumber(lua, "-2.1e+5", -210000)); + ASSERT_TRUE(checkNumber(lua, "+2.1e+5", 210000)); + ASSERT_TRUE(checkNumber(lua, "0.27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "-0.27", -0.27)); + ASSERT_TRUE(checkNumber(lua, "+0.27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "2.7", 2.7)); + ASSERT_TRUE(checkNumber(lua, "-2.7", -2.7)); + ASSERT_TRUE(checkNumber(lua, "+2.7", 2.7)); + ASSERT_TRUE(checkNumber(lua, ".27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "-.27", -0.27)); + ASSERT_TRUE(checkNumber(lua, "+.27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "27.", 27.0)); + ASSERT_TRUE(checkNumber(lua, "-27.", -27.0)); + ASSERT_TRUE(checkNumber(lua, "+27.", 27.0)); + + ASSERT_TRUE(checkNan(lua, ".nan")); + ASSERT_TRUE(checkNan(lua, ".NaN")); + ASSERT_TRUE(checkNan(lua, ".NAN")); + ASSERT_FALSE(checkNan(lua, "nan")); + ASSERT_FALSE(checkNan(lua, ".nAn")); + ASSERT_TRUE(checkString(lua, "'.nan'", ".nan")); + ASSERT_TRUE(checkString(lua, ".nAn")); + + ASSERT_TRUE(checkNumber(lua, "1.7976931348623157E+308", std::numeric_limits::max())); + ASSERT_TRUE(checkNumber(lua, "-1.7976931348623157E+308", std::numeric_limits::lowest())); + ASSERT_TRUE(checkNumber(lua, "2.2250738585072014e-308", std::numeric_limits::min())); + ASSERT_TRUE(checkNumber(lua, ".inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "+.inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "-.inf", -std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, ".Inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "+.Inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "-.Inf", -std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, ".INF", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "+.INF", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "-.INF", -std::numeric_limits::infinity())); + ASSERT_TRUE(checkString(lua, ".INf")); + ASSERT_TRUE(checkString(lua, "-.INf")); + ASSERT_TRUE(checkString(lua, "+.INf")); + + ASSERT_TRUE(checkBool(lua, "true", true)); + ASSERT_TRUE(checkBool(lua, "false", false)); + ASSERT_TRUE(checkBool(lua, "True", true)); + ASSERT_TRUE(checkBool(lua, "False", false)); + ASSERT_TRUE(checkBool(lua, "TRUE", true)); + ASSERT_TRUE(checkBool(lua, "FALSE", false)); + ASSERT_TRUE(checkString(lua, "y")); + ASSERT_TRUE(checkString(lua, "n")); + ASSERT_TRUE(checkString(lua, "On")); + ASSERT_TRUE(checkString(lua, "Off")); + ASSERT_TRUE(checkString(lua, "YES")); + ASSERT_TRUE(checkString(lua, "NO")); + ASSERT_TRUE(checkString(lua, "TrUe")); + ASSERT_TRUE(checkString(lua, "FaLsE")); + ASSERT_TRUE(checkString(lua, "'true'", "true")); + } + + TEST(LuaUtilYamlLoader, DepthLimit) + { + sol::state lua; + + const std::string input = R"( + array1: &array1_alias + [ + <: *array1_alias, + foo + ] + )"; + + bool depthExceptionThrown = false; + try + { + YAML::Node root = YAML::Load(input); + sol::object result = LuaUtil::loadYaml(input, lua); + } + catch (const std::runtime_error& e) + { + ASSERT_EQ(std::string(e.what()), "Maximum layers depth exceeded, probably caused by a circular reference"); + depthExceptionThrown = true; + } + + ASSERT_TRUE(depthExceptionThrown); + } + + TEST(LuaUtilYamlLoader, Collections) + { + sol::state lua; + + sol::object map = LuaUtil::loadYaml("{ x: , y: 2, 4: 5 }", lua); + ASSERT_EQ(map.as()["x"], sol::nil); + ASSERT_EQ(map.as()["y"], 2); + ASSERT_EQ(map.as()[4], 5); + + sol::object array = LuaUtil::loadYaml("[ 3, 4 ]", lua); + ASSERT_EQ(array.as()[1], 3); + + sol::object emptyTable = LuaUtil::loadYaml("{}", lua); + ASSERT_TRUE(emptyTable.as().empty()); + + sol::object emptyArray = LuaUtil::loadYaml("[]", lua); + ASSERT_TRUE(emptyArray.as().empty()); + + ASSERT_THROW(LuaUtil::loadYaml("{ null: 1 }", lua), std::runtime_error); + ASSERT_THROW(LuaUtil::loadYaml("{ .nan: 1 }", lua), std::runtime_error); + + const std::string scalarArrayInput = R"( + - First Scalar + - 1 + - true)"; + + sol::object scalarArray = LuaUtil::loadYaml(scalarArrayInput, lua); + ASSERT_EQ(scalarArray.as()[1], std::string("First Scalar")); + ASSERT_EQ(scalarArray.as()[2], 1); + ASSERT_EQ(scalarArray.as()[3], true); + + const std::string scalarMapWithCommentsInput = R"( + string: 'str' # String value + integer: 65 # Integer value + float: 0.278 # Float value + bool: false # Boolean value)"; + + sol::object scalarMapWithComments = LuaUtil::loadYaml(scalarMapWithCommentsInput, lua); + ASSERT_EQ(scalarMapWithComments.as()["string"], std::string("str")); + ASSERT_EQ(scalarMapWithComments.as()["integer"], 65); + ASSERT_EQ(scalarMapWithComments.as()["float"], 0.278); + ASSERT_EQ(scalarMapWithComments.as()["bool"], false); + + const std::string mapOfArraysInput = R"( + x: + - 2 + - 7 + - true + y: + - aaa + - false + - 1)"; + + sol::object mapOfArrays = LuaUtil::loadYaml(mapOfArraysInput, lua); + ASSERT_EQ(mapOfArrays.as()["x"][3], true); + ASSERT_EQ(mapOfArrays.as()["y"][1], std::string("aaa")); + + const std::string arrayOfMapsInput = R"( + - + name: Name1 + hr: 65 + avg: 0.278 + - + name: Name2 + hr: 63 + avg: 0.288)"; + + sol::object arrayOfMaps = LuaUtil::loadYaml(arrayOfMapsInput, lua); + ASSERT_EQ(arrayOfMaps.as()[1]["avg"], 0.278); + ASSERT_EQ(arrayOfMaps.as()[2]["name"], std::string("Name2")); + + const std::string arrayOfArraysInput = R"( + - [Name1, 65, 0.278] + - [Name2 , 63, 0.288])"; + + sol::object arrayOfArrays = LuaUtil::loadYaml(arrayOfArraysInput, lua); + ASSERT_EQ(arrayOfArrays.as()[1][2], 65); + ASSERT_EQ(arrayOfArrays.as()[2][1], std::string("Name2")); + + const std::string mapOfMapsInput = R"( + Name1: {hr: 65, avg: 0.278} + Name2 : { + hr: 63, + avg: 0.288, + })"; + + sol::object mapOfMaps = LuaUtil::loadYaml(mapOfMapsInput, lua); + ASSERT_EQ(mapOfMaps.as()["Name1"]["hr"], 65); + ASSERT_EQ(mapOfMaps.as()["Name2"]["avg"], 0.288); + } + + TEST(LuaUtilYamlLoader, Structures) + { + sol::state lua; + + const std::string twoDocumentsInput + = "---\n" + " - First Scalar\n" + " - 2\n" + " - true\n" + "\n" + "---\n" + " - Second Scalar\n" + " - 3\n" + " - false"; + + sol::object twoDocuments = LuaUtil::loadYaml(twoDocumentsInput, lua); + ASSERT_EQ(twoDocuments.as()[1][1], std::string("First Scalar")); + ASSERT_EQ(twoDocuments.as()[2][3], false); + + const std::string anchorInput = R"(--- + x: + - Name1 + # Following node labeled as "a" + - &a Value1 + y: + - *a # Subsequent occurrence + - Name2)"; + + sol::object anchor = LuaUtil::loadYaml(anchorInput, lua); + ASSERT_EQ(anchor.as()["y"][1], std::string("Value1")); + + const std::string compoundKeyInput = R"( + ? - String1 + - String2 + : - 1 + + ? [ String3, + String4 ] + : [ 2, 3, 4 ])"; + + ASSERT_THROW(LuaUtil::loadYaml(compoundKeyInput, lua), std::runtime_error); + + const std::string compactNestedMappingInput = R"( + - item : Item1 + quantity: 2 + - item : Item2 + quantity: 4 + - item : Item3 + quantity: 11)"; + + sol::object compactNestedMapping = LuaUtil::loadYaml(compactNestedMappingInput, lua); + ASSERT_EQ(compactNestedMapping.as()[2]["quantity"], 4); + } + + TEST(LuaUtilYamlLoader, Scalars) + { + sol::state lua; + + const std::string literalScalarInput = R"(--- | + a + b + c)"; + + ASSERT_TRUE(checkString(lua, literalScalarInput, "a\nb\nc")); + + const std::string foldedScalarInput = R"(--- > + a + b + c)"; + + ASSERT_TRUE(checkString(lua, foldedScalarInput, "a b c")); + + const std::string multiLinePlanarScalarsInput = R"( + plain: + This unquoted scalar + spans many lines. + + quoted: "So does this + quoted scalar.\n")"; + + sol::object multiLinePlanarScalars = LuaUtil::loadYaml(multiLinePlanarScalarsInput, lua); + ASSERT_TRUE( + multiLinePlanarScalars.as()["plain"] == std::string("This unquoted scalar spans many lines.")); + ASSERT_TRUE(multiLinePlanarScalars.as()["quoted"] == std::string("So does this quoted scalar.\n")); + } +} diff --git a/apps/openmw_test_suite/openmw_test_suite.cpp b/apps/components_tests/main.cpp similarity index 62% rename from apps/openmw_test_suite/openmw_test_suite.cpp rename to apps/components_tests/main.cpp index a2b9f1ae73..dcfb2e9ba9 100644 --- a/apps/openmw_test_suite/openmw_test_suite.cpp +++ b/apps/components_tests/main.cpp @@ -1,20 +1,16 @@ -#include +#include +#include +#include +#include -#include "components/misc/strings/conversion.hpp" -#include "components/settings/parser.hpp" -#include "components/settings/values.hpp" +#include #include -#ifdef WIN32 -// we cannot use GTEST_API_ before main if we're building standalone exe application, -// and we're linking GoogleTest / GoogleMock as DLLs and not linking gtest_main / gmock_main int main(int argc, char** argv) { -#else -GTEST_API_ int main(int argc, char** argv) -{ -#endif + Log::sMinDebugLevel = Debug::getDebugLevel(); + const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files" / Misc::StringUtils::stringToU8String("settings-default.cfg"); diff --git a/apps/openmw_test_suite/misc/compression.cpp b/apps/components_tests/misc/compression.cpp similarity index 100% rename from apps/openmw_test_suite/misc/compression.cpp rename to apps/components_tests/misc/compression.cpp diff --git a/apps/openmw_test_suite/misc/progressreporter.cpp b/apps/components_tests/misc/progressreporter.cpp similarity index 100% rename from apps/openmw_test_suite/misc/progressreporter.cpp rename to apps/components_tests/misc/progressreporter.cpp diff --git a/apps/openmw_test_suite/misc/test_endianness.cpp b/apps/components_tests/misc/test_endianness.cpp similarity index 100% rename from apps/openmw_test_suite/misc/test_endianness.cpp rename to apps/components_tests/misc/test_endianness.cpp diff --git a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp b/apps/components_tests/misc/test_resourcehelpers.cpp similarity index 70% rename from apps/openmw_test_suite/misc/test_resourcehelpers.cpp rename to apps/components_tests/misc/test_resourcehelpers.cpp index 0db147d8a3..05079ae875 100644 --- a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp +++ b/apps/components_tests/misc/test_resourcehelpers.cpp @@ -1,5 +1,6 @@ -#include "../testing_util.hpp" -#include "components/misc/resourcehelpers.hpp" +#include +#include + #include namespace @@ -7,27 +8,24 @@ namespace using namespace Misc::ResourceHelpers; TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected) { - std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { "sound/bar.wav", nullptr } }); - EXPECT_EQ(correctSoundPath("sound/bar.wav", mVFS.get()), "sound/bar.wav"); + constexpr VFS::Path::NormalizedView path("sound/bar.wav"); + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { path, nullptr } }); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/bar.wav"); } TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected) { - std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { "sound/foo.mp3", nullptr } }); - EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3"); + constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3"); + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { mp3, nullptr } }); + constexpr VFS::Path::NormalizedView wav("sound/foo.wav"); + EXPECT_EQ(correctSoundPath(wav, *mVFS), "sound/foo.mp3"); } TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs) { std::unique_ptr mVFS = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3"); - } - - TEST(CorrectSoundPath, correct_path_normalize_paths) - { - std::unique_ptr mVFS = TestingOpenMW::createTestVFS({}); - EXPECT_EQ(correctSoundPath("sound\\foo.wav", mVFS.get()), "sound/foo.mp3"); - EXPECT_EQ(correctSoundPath("SOUND\\foo.WAV", mVFS.get()), "sound/foo.mp3"); + constexpr VFS::Path::NormalizedView path("sound/foo.wav"); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); } namespace diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/components_tests/misc/test_stringops.cpp similarity index 100% rename from apps/openmw_test_suite/misc/test_stringops.cpp rename to apps/components_tests/misc/test_stringops.cpp diff --git a/apps/components_tests/misc/testmathutil.cpp b/apps/components_tests/misc/testmathutil.cpp new file mode 100644 index 0000000000..c4b545c2f4 --- /dev/null +++ b/apps/components_tests/misc/testmathutil.cpp @@ -0,0 +1,194 @@ +#include + +#include + +#include +#include + +#include +#include + +MATCHER_P2(Vec3fEq, other, precision, "") +{ + return std::abs(arg.x() - other.x()) < precision && std::abs(arg.y() - other.y()) < precision + && std::abs(arg.z() - other.z()) < precision; +} + +namespace testing +{ + template <> + inline testing::Message& Message::operator<<(const osg::Vec3f& value) + { + return (*this) << std::setprecision(std::numeric_limits::max_exponent10) << "osg::Vec3f(" << value.x() + << ", " << value.y() << ", " << value.z() << ')'; + } + + template <> + inline testing::Message& Message::operator<<(const osg::Quat& value) + { + return (*this) << std::setprecision(std::numeric_limits::max_exponent10) << "osg::Quat(" << value.x() + << ", " << value.y() << ", " << value.z() << ", " << value.w() << ')'; + } +} + +namespace Misc +{ + namespace + { + using namespace testing; + + struct MiscToEulerAnglesXZQuatTest : TestWithParam> + { + }; + + TEST_P(MiscToEulerAnglesXZQuatTest, shouldReturnValueCloseTo) + { + const osg::Vec3f result = toEulerAnglesXZ(GetParam().first); + EXPECT_THAT(result, Vec3fEq(GetParam().second, 1e-6)) + << "toEulerAnglesXZ(" << GetParam().first << ") = " << result; + } + + const std::pair eulerAnglesXZQuat[] = { + { + osg::Quat(1, 0, 0, 0), + osg::Vec3f(0, 0, osg::PI), + }, + { + osg::Quat(0, 1, 0, 0), + osg::Vec3f(0, 0, 0), + }, + { + osg::Quat(0, 0, 1, 0), + osg::Vec3f(0, 0, osg::PI), + }, + { + osg::Quat(0, 0, 0, 1), + osg::Vec3f(0, 0, 0), + }, + { + osg::Quat(-0.5, -0.5, -0.5, -0.5), + osg::Vec3f(-osg::PI_2f, 0, 0), + }, + { + osg::Quat(0.5, -0.5, -0.5, -0.5), + osg::Vec3f(0, 0, -osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, -0.5, -0.5), + osg::Vec3f(osg::PI_2f, 0, 0), + }, + { + osg::Quat(0.5, 0.5, 0.5, -0.5), + osg::Vec3f(0, 0, osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, 0.5, 0.5), + osg::Vec3f(-osg::PI_2f, 0, 0), + }, + { + // normalized osg::Quat(0.1, 0.2, 0.3, 0.4) + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.72972762584686279296875f, 0, -1.10714876651763916015625f), + }, + { + osg::Quat(-0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.13373161852359771728515625f, 0, -1.2277724742889404296875f), + }, + { + osg::Quat(0.18257418583505536, -0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.13373161852359771728515625f, 0, -1.2277724742889404296875f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, -0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.13373161852359771728515625f, 0, 1.2277724742889404296875f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, -0.73029674334022143), + osg::Vec3f(-0.13373161852359771728515625, 0, 1.2277724742889404296875f), + }, + { + osg::Quat(0.246736, -0.662657, -0.662667, 0.246739), + osg::Vec3f(-osg::PI_2f, 0, 2.5199801921844482421875f), + }, + }; + + INSTANTIATE_TEST_SUITE_P(FromQuat, MiscToEulerAnglesXZQuatTest, ValuesIn(eulerAnglesXZQuat)); + + struct MiscToEulerAnglesZYXQuatTest : TestWithParam> + { + }; + + TEST_P(MiscToEulerAnglesZYXQuatTest, shouldReturnValueCloseTo) + { + const osg::Vec3f result = toEulerAnglesZYX(GetParam().first); + EXPECT_THAT(result, Vec3fEq(GetParam().second, std::numeric_limits::epsilon())) + << "toEulerAnglesZYX(" << GetParam().first << ") = " << result; + } + + const std::pair eulerAnglesZYXQuat[] = { + { + osg::Quat(1, 0, 0, 0), + osg::Vec3f(osg::PI, 0, 0), + }, + { + osg::Quat(0, 1, 0, 0), + osg::Vec3f(osg::PI, 0, osg::PI), + }, + { + osg::Quat(0, 0, 1, 0), + osg::Vec3f(0, 0, osg::PI), + }, + { + osg::Quat(0, 0, 0, 1), + osg::Vec3f(0, 0, 0), + }, + { + osg::Quat(-0.5, -0.5, -0.5, -0.5), + osg::Vec3f(0, -osg::PI_2f, -osg::PI_2f), + }, + { + osg::Quat(0.5, -0.5, -0.5, -0.5), + osg::Vec3f(osg::PI_2f, 0, -osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, -0.5, -0.5), + osg::Vec3f(0, osg::PI_2f, -osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, 0.5, -0.5), + osg::Vec3f(osg::PI_2f, 0, osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, 0.5, 0.5), + osg::Vec3f(0, -osg::PI_2f, -osg::PI_2f), + }, + { + // normalized osg::Quat(0.1, 0.2, 0.3, 0.4) + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.1973955929279327392578125f, -0.8232119083404541015625f, -1.37340080738067626953125f), + }, + { + osg::Quat(-0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.78539812564849853515625f, -0.339836895465850830078125f, -1.428899288177490234375f), + }, + { + osg::Quat(0.18257418583505536, -0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.78539812564849853515625f, 0.339836895465850830078125f, -1.428899288177490234375f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, -0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.78539812564849853515625f, -0.339836895465850830078125f, 1.428899288177490234375f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, -0.73029674334022143), + osg::Vec3f(0.78539812564849853515625f, 0.339836895465850830078125f, 1.428899288177490234375f), + }, + { + osg::Quat(0.246736, -0.662657, 0.246739, -0.662667), + osg::Vec3f(0.06586204469203948974609375f, -osg::PI_2f, 0.64701664447784423828125f), + }, + }; + + INSTANTIATE_TEST_SUITE_P(FromQuat, MiscToEulerAnglesZYXQuatTest, ValuesIn(eulerAnglesZYXQuat)); + } +} diff --git a/apps/components_tests/nif/node.hpp b/apps/components_tests/nif/node.hpp new file mode 100644 index 0000000000..4e21698501 --- /dev/null +++ b/apps/components_tests/nif/node.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_TEST_SUITE_NIF_NODE_H +#define OPENMW_TEST_SUITE_NIF_NODE_H + +#include +#include + +namespace Nif::Testing +{ + inline void init(NiTransform& value) + { + value = NiTransform::getIdentity(); + } + + inline void init(Extra& value) + { + value.mNext = ExtraPtr(nullptr); + } + + inline void init(NiObjectNET& value) + { + value.mExtra = ExtraPtr(nullptr); + value.mExtraList = ExtraList(); + value.mController = NiTimeControllerPtr(nullptr); + } + + inline void init(NiAVObject& value) + { + init(static_cast(value)); + value.mFlags = 0; + init(value.mTransform); + } + + inline void init(NiGeometry& value) + { + init(static_cast(value)); + value.mData = NiGeometryDataPtr(nullptr); + value.mSkin = NiSkinInstancePtr(nullptr); + } + + inline void init(NiTriShape& value) + { + init(static_cast(value)); + value.recType = RC_NiTriShape; + } + + inline void init(NiTriStrips& value) + { + init(static_cast(value)); + value.recType = RC_NiTriStrips; + } + + inline void init(NiSkinInstance& value) + { + value.mData = NiSkinDataPtr(nullptr); + value.mRoot = NiAVObjectPtr(nullptr); + value.mPartitions = NiSkinPartitionPtr(nullptr); + } + + inline void init(NiTimeController& value) + { + value.mNext = NiTimeControllerPtr(nullptr); + value.mFlags = 0; + value.mFrequency = 0; + value.mPhase = 0; + value.mTimeStart = 0; + value.mTimeStop = 0; + value.mTarget = NiObjectNETPtr(nullptr); + } +} + +#endif diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/components_tests/nifloader/testbulletnifloader.cpp similarity index 61% rename from apps/openmw_test_suite/nifloader/testbulletnifloader.cpp rename to apps/components_tests/nifloader/testbulletnifloader.cpp index 25df366d23..c6df103858 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/components_tests/nifloader/testbulletnifloader.cpp @@ -144,6 +144,12 @@ namespace Resource return stream << "}}"; } + static std::ostream& operator<<(std::ostream& stream, const ScaledTriangleMeshShape& value) + { + return stream << "Resource::ScaledTriangleMeshShape {" << value.getLocalScaling() << ", " + << value.getChildShape() << "}"; + } + static bool operator==(const CollisionBox& l, const CollisionBox& r) { const auto tie = [](const CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; @@ -169,6 +175,10 @@ static std::ostream& operator<<(std::ostream& stream, const btCollisionShape& va if (const auto casted = dynamic_cast(&value)) return stream << *casted; break; + case SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto casted = dynamic_cast(&value)) + return stream << *casted; + break; } return stream << "btCollisionShape {" << value.getShapeType() << "}"; } @@ -249,6 +259,12 @@ static bool operator==(const btBvhTriangleMeshShape& lhs, const btBvhTriangleMes && lhs.getOwnsBvh() == rhs.getOwnsBvh() && isNear(getTriangles(lhs), getTriangles(rhs)); } +static bool operator==(const btScaledBvhTriangleMeshShape& lhs, const btScaledBvhTriangleMeshShape& rhs) +{ + return isNear(lhs.getLocalScaling(), rhs.getLocalScaling()) + && compareObjects(lhs.getChildShape(), rhs.getChildShape()); +} + static bool operator==(const btCollisionShape& lhs, const btCollisionShape& rhs) { if (lhs.getShapeType() != rhs.getShapeType()) @@ -264,6 +280,11 @@ static bool operator==(const btCollisionShape& lhs, const btCollisionShape& rhs) if (const auto rhsCasted = dynamic_cast(&rhs)) return *lhsCasted == *rhsCasted; return false; + case SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto lhsCasted = dynamic_cast(&lhs)) + if (const auto rhsCasted = dynamic_cast(&rhs)) + return *lhsCasted == *rhsCasted; + return false; } return false; } @@ -274,19 +295,22 @@ namespace using namespace Nif::Testing; using NifBullet::BulletNifLoader; - void copy(const btTransform& src, Nif::Transformation& dst) + constexpr VFS::Path::NormalizedView testNif("test.nif"); + constexpr VFS::Path::NormalizedView xtestNif("xtest.nif"); + + void copy(const btTransform& src, Nif::NiTransform& dst) { - dst.pos = osg::Vec3f(src.getOrigin().x(), src.getOrigin().y(), src.getOrigin().z()); + dst.mTranslation = osg::Vec3f(src.getOrigin().x(), src.getOrigin().y(), src.getOrigin().z()); for (int row = 0; row < 3; ++row) for (int column = 0; column < 3; ++column) - dst.rotation.mValues[row][column] = src.getBasis().getRow(row)[column]; + dst.mRotation.mValues[row][column] = src.getBasis().getRow(row)[column]; } struct TestBulletNifLoader : Test { BulletNifLoader mLoader; - Nif::Node mNode; - Nif::Node mNode2; + Nif::NiAVObject mNode; + Nif::NiAVObject mNode2; Nif::NiNode mNiNode; Nif::NiNode mNiNode2; Nif::NiNode mNiNode3; @@ -300,7 +324,7 @@ namespace Nif::NiStringExtraData mNiStringExtraData; Nif::NiStringExtraData mNiStringExtraData2; Nif::NiIntegerExtraData mNiIntegerExtraData; - Nif::Controller mController; + Nif::NiTimeController mController; btTransform mTransform{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(1, 2, 3) }; btTransform mTransformScale2{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(2, 4, 6) }; btTransform mTransformScale3{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(3, 6, 9) }; @@ -323,29 +347,29 @@ namespace init(mController); mNiTriShapeData.recType = Nif::RC_NiTriShapeData; - mNiTriShapeData.vertices = { osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0) }; + mNiTriShapeData.mVertices = { osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0) }; mNiTriShapeData.mNumTriangles = 1; - mNiTriShapeData.triangles = { 0, 1, 2 }; - mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + mNiTriShapeData.mTriangles = { 0, 1, 2 }; + mNiTriShape.mData = Nif::NiGeometryDataPtr(&mNiTriShapeData); mNiTriShapeData2.recType = Nif::RC_NiTriShapeData; - mNiTriShapeData2.vertices = { osg::Vec3f(0, 0, 1), osg::Vec3f(1, 0, 1), osg::Vec3f(1, 1, 1) }; + mNiTriShapeData2.mVertices = { osg::Vec3f(0, 0, 1), osg::Vec3f(1, 0, 1), osg::Vec3f(1, 1, 1) }; mNiTriShapeData2.mNumTriangles = 1; - mNiTriShapeData2.triangles = { 0, 1, 2 }; - mNiTriShape2.data = Nif::NiGeometryDataPtr(&mNiTriShapeData2); + mNiTriShapeData2.mTriangles = { 0, 1, 2 }; + mNiTriShape2.mData = Nif::NiGeometryDataPtr(&mNiTriShapeData2); mNiTriStripsData.recType = Nif::RC_NiTriStripsData; - mNiTriStripsData.vertices + mNiTriStripsData.mVertices = { osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0) }; mNiTriStripsData.mNumTriangles = 2; - mNiTriStripsData.strips = { { 0, 1, 2, 3 } }; - mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + mNiTriStripsData.mStrips = { { 0, 1, 2, 3 } }; + mNiTriStrips.mData = Nif::NiGeometryDataPtr(&mNiTriStripsData); } }; TEST_F(TestBulletNifLoader, for_zero_num_roots_should_return_default) { - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mHash = mHash; const auto result = mLoader.load(file); @@ -359,7 +383,7 @@ namespace TEST_F(TestBulletNifLoader, should_ignore_nullptr_root) { - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(nullptr); file.mHash = mHash; @@ -372,7 +396,7 @@ namespace TEST_F(TestBulletNifLoader, for_default_root_nif_node_should_return_default) { - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNode); file.mHash = mHash; @@ -387,7 +411,7 @@ namespace { mNode.recType = Nif::RC_RootCollisionNode; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNode); file.mHash = mHash; @@ -400,7 +424,7 @@ namespace TEST_F(TestBulletNifLoader, for_default_root_nif_node_and_filename_starting_with_x_should_return_default) { - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNode); file.mHash = mHash; @@ -411,43 +435,94 @@ namespace EXPECT_EQ(*result, expected); } + TEST_F(TestBulletNifLoader, for_root_bounding_box_should_return_shape_with_bounding_box_data) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + + Nif::NIFFile file(testNif); + file.mRoots.push_back(&mNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_child_bounding_box_should_return_shape_with_bounding_box_data) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode) }; + + Nif::NIFFile file(testNif); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_root_with_bounds_and_child_bounding_box_should_use_bounding_box) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); + + mNiNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiNode.mBounds.mBox.mExtents = osg::Vec3f(4, 5, 6); + mNiNode.mBounds.mBox.mCenter = osg::Vec3f(-4, -5, -6); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode) }; + + Nif::NIFFile file(testNif); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + TEST_F( - TestBulletNifLoader, for_root_nif_node_with_bounding_box_should_return_shape_with_compound_shape_and_box_inside) + TestBulletNifLoader, for_root_and_two_children_where_both_with_bounds_but_one_is_bounding_box_use_bounding_box) { - mNode.hasBounds = true; - mNode.flags |= Nif::Node::Flag_BBoxCollision; - mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNode); - file.mHash = mHash; + mNode2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode2.mBounds.mBox.mExtents = osg::Vec3f(4, 5, 6); + mNode2.mBounds.mBox.mCenter = osg::Vec3f(-4, -5, -6); + mNode2.mParents.push_back(&mNiNode); - const auto result = mLoader.load(file); + mNiNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiNode.mBounds.mBox.mExtents = osg::Vec3f(7, 8, 9); + mNiNode.mBounds.mBox.mCenter = osg::Vec3f(-7, -8, -9); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode), Nif::NiAVObjectPtr(&mNode2) }; - Resource::BulletShape expected; - expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); - - EXPECT_EQ(*result, expected); - } - - TEST_F(TestBulletNifLoader, for_child_nif_node_with_bounding_box) - { - mNode.hasBounds = true; - mNode.flags |= Nif::Node::Flag_BBoxCollision; - mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); - mNode.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNode) })); - - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -456,109 +531,30 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, - for_root_and_child_nif_node_with_bounding_box_but_root_without_flag_should_use_child_bounds) + for_root_and_two_children_where_both_with_bounds_but_second_is_bounding_box_use_bounding_box) { - mNode.hasBounds = true; - mNode.flags |= Nif::Node::Flag_BBoxCollision; - mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); - mNode.parents.push_back(&mNiNode); + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); - mNiNode.hasBounds = true; - mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNiNode.bounds.box.extents = osg::Vec3f(4, 5, 6); - mNiNode.bounds.box.center = osg::Vec3f(-4, -5, -6); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNode) })); + mNode2.mName = "Bounding Box"; + mNode2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode2.mBounds.mBox.mExtents = osg::Vec3f(4, 5, 6); + mNode2.mBounds.mBox.mCenter = osg::Vec3f(-4, -5, -6); + mNode2.mParents.push_back(&mNiNode); - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiNode); - file.mHash = mHash; + mNiNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiNode.mBounds.mBox.mExtents = osg::Vec3f(7, 8, 9); + mNiNode.mBounds.mBox.mCenter = osg::Vec3f(-7, -8, -9); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode), Nif::NiAVObjectPtr(&mNode2) }; - const auto result = mLoader.load(file); - - Resource::BulletShape expected; - expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); - - EXPECT_EQ(*result, expected); - } - - TEST_F(TestBulletNifLoader, - for_root_and_two_children_where_both_with_bounds_but_only_first_with_flag_should_use_first_bounds) - { - mNode.hasBounds = true; - mNode.flags |= Nif::Node::Flag_BBoxCollision; - mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); - mNode.parents.push_back(&mNiNode); - - mNode2.hasBounds = true; - mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); - mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); - mNode2.parents.push_back(&mNiNode); - - mNiNode.hasBounds = true; - mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNiNode.bounds.box.extents = osg::Vec3f(7, 8, 9); - mNiNode.bounds.box.center = osg::Vec3f(-7, -8, -9); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNode), Nif::NodePtr(&mNode2) })); - - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiNode); - file.mHash = mHash; - - const auto result = mLoader.load(file); - - Resource::BulletShape expected; - expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); - std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape.reset(shape.release()); - - EXPECT_EQ(*result, expected); - } - - TEST_F(TestBulletNifLoader, - for_root_and_two_children_where_both_with_bounds_but_only_second_with_flag_should_use_second_bounds) - { - mNode.hasBounds = true; - mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); - mNode.parents.push_back(&mNiNode); - - mNode2.hasBounds = true; - mNode2.flags |= Nif::Node::Flag_BBoxCollision; - mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); - mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); - mNode2.parents.push_back(&mNiNode); - - mNiNode.hasBounds = true; - mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNiNode.bounds.box.extents = osg::Vec3f(7, 8, 9); - mNiNode.bounds.box.center = osg::Vec3f(-7, -8, -9); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNode), Nif::NodePtr(&mNode2) })); - - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -567,38 +563,30 @@ namespace Resource::BulletShape expected; expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6); expected.mCollisionBox.mCenter = osg::Vec3f(-4, -5, -6); - std::unique_ptr box(new btBoxShape(btVector3(4, 5, 6))); - std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release()); - expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_root_nif_node_with_bounds_but_without_flag_should_return_shape_with_bounds_but_with_null_collision_shape) + TEST_F(TestBulletNifLoader, for_root_nif_node_with_bounds_should_return_shape_with_null_collision_shape) { - mNode.hasBounds = true; - mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNode); file.mHash = mHash; const auto result = mLoader.load(file); Resource::BulletShape expected; - expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_tri_shape_root_node_should_return_static_shape) { - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiTriShape); file.mHash = mHash; @@ -608,7 +596,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -616,33 +606,38 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_tri_shape_root_node_with_bounds_should_return_static_shape_with_bounds_but_with_null_collision_shape) + TEST_F(TestBulletNifLoader, for_tri_shape_root_node_with_bounds_should_return_static_shape) { - mNiTriShape.hasBounds = true; - mNiTriShape.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; - mNiTriShape.bounds.box.extents = osg::Vec3f(1, 2, 3); - mNiTriShape.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNiTriShape.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiTriShape.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNiTriShape.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiTriShape); file.mHash = mHash; const auto result = mLoader.load(file); + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + Resource::BulletShape expected; - expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + expected.mCollisionShape.reset(compound.release()); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_tri_shape_child_node_should_return_static_shape) { - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -652,7 +647,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -662,12 +659,12 @@ namespace TEST_F(TestBulletNifLoader, for_nested_tri_shape_child_should_return_static_shape) { - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiNode2) })); - mNiNode2.parents.push_back(&mNiNode); - mNiNode2.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - mNiTriShape.parents.push_back(&mNiNode2); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiNode2) }; + mNiNode2.mParents.push_back(&mNiNode); + mNiNode2.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiTriShape.mParents.push_back(&mNiNode2); - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -677,7 +674,9 @@ namespace triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -687,12 +686,11 @@ namespace TEST_F(TestBulletNifLoader, for_two_tri_shape_children_should_return_static_shape_with_all_meshes) { - mNiTriShape.parents.push_back(&mNiNode); - mNiTriShape2.parents.push_back(&mNiNode); - mNiNode.children - = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), Nif::NodePtr(&mNiTriShape2) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape2.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -703,9 +701,13 @@ namespace std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + auto triShape2 = std::make_unique(triangles2.release(), true); + compound->addChildShape( - btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape2.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -716,11 +718,11 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_and_not_empty_skin_should_return_static_shape) { - mNiTriShape.skin = Nif::NiSkinInstancePtr(&mNiSkinInstance); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mSkin = Nif::NiSkinInstancePtr(&mNiSkinInstance); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -729,7 +731,9 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -739,10 +743,10 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_root_node_and_filename_starting_with_x_should_return_animated_shape) { - copy(mTransform, mNiTriShape.trafo); - mNiTriShape.trafo.scale = 3; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiTriShape); file.mHash = mHash; @@ -751,9 +755,8 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(3, 3, 3)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransform, mesh.release()); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(3, 3, 3))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -763,13 +766,13 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_should_return_animated_shape) { - copy(mTransform, mNiTriShape.trafo); - mNiTriShape.trafo.scale = 3; - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - mNiNode.trafo.scale = 4; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.mTransform.mScale = 4; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -778,9 +781,9 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale4, mesh.release()); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -791,20 +794,17 @@ namespace TEST_F( TestBulletNifLoader, for_two_tri_shape_children_nodes_and_filename_starting_with_x_should_return_animated_shape) { - copy(mTransform, mNiTriShape.trafo); - mNiTriShape.trafo.scale = 3; - mNiTriShape.parents.push_back(&mNiNode); + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); - copy(mTransform, mNiTriShape2.trafo); - mNiTriShape2.trafo.scale = 3; - mNiTriShape2.parents.push_back(&mNiNode); + copy(mTransform, mNiTriShape2.mTransform); + mNiTriShape2.mTransform.mScale = 3; + mNiTriShape2.mParents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ - Nif::NodePtr(&mNiTriShape), - Nif::NodePtr(&mNiTriShape2), - })); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) }; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -813,16 +813,14 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(3, 3, 3)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); - mesh2->setLocalScaling(btVector3(3, 3, 3)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransform, mesh.release()); - shape->addChildShape(mTransform, mesh2.release()); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(3, 3, 3))); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(3, 3, 3))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -833,15 +831,15 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_controller_should_return_animated_shape) { mController.recType = Nif::RC_NiKeyframeController; - mController.flags |= Nif::Controller::Flag_Active; - copy(mTransform, mNiTriShape.trafo); - mNiTriShape.trafo.scale = 3; - mNiTriShape.parents.push_back(&mNiNode); - mNiTriShape.controller = Nif::ControllerPtr(&mController); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - mNiNode.trafo.scale = 4; + mController.mFlags |= Nif::NiTimeController::Flag_Active; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mController = Nif::NiTimeControllerPtr(&mController); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.mTransform.mScale = 4; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -850,9 +848,9 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale4, mesh.release()); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; @@ -863,21 +861,21 @@ namespace TEST_F(TestBulletNifLoader, for_two_tri_shape_children_nodes_where_one_with_controller_should_return_animated_shape) { mController.recType = Nif::RC_NiKeyframeController; - mController.flags |= Nif::Controller::Flag_Active; - copy(mTransform, mNiTriShape.trafo); - mNiTriShape.trafo.scale = 3; - mNiTriShape.parents.push_back(&mNiNode); - copy(mTransform, mNiTriShape2.trafo); - mNiTriShape2.trafo.scale = 3; - mNiTriShape2.parents.push_back(&mNiNode); - mNiTriShape2.controller = Nif::ControllerPtr(&mController); - mNiNode.children = Nif::NodeList(std::vector({ - Nif::NodePtr(&mNiTriShape), - Nif::NodePtr(&mNiTriShape2), - })); - mNiNode.trafo.scale = 4; + mController.mFlags |= Nif::NiTimeController::Flag_Active; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + copy(mTransform, mNiTriShape2.mTransform); + mNiTriShape2.mTransform.mScale = 3; + mNiTriShape2.mParents.push_back(&mNiNode); + mNiTriShape2.mController = Nif::NiTimeControllerPtr(&mController); + mNiNode.mChildren = Nif::NiAVObjectList{ + Nif::NiAVObjectPtr(&mNiTriShape), + Nif::NiAVObjectPtr(&mNiTriShape2), + }; + mNiNode.mTransform.mScale = 4; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -886,16 +884,16 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); - mesh->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); - mesh2->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale4, mesh.release()); - shape->addChildShape(mTransformScale4, mesh2.release()); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 1 } }; @@ -905,10 +903,10 @@ namespace TEST_F(TestBulletNifLoader, should_add_static_mesh_to_existing_compound_mesh) { - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mRoots.push_back(&mNiTriShape2); file.mHash = mHash; @@ -917,14 +915,17 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); compound->addChildShape( - btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -936,11 +937,11 @@ namespace TEST_F( TestBulletNifLoader, for_root_avoid_node_and_tri_shape_child_node_should_return_shape_with_null_collision_shape) { - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; mNiNode.recType = Nif::RC_AvoidNode; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -948,9 +949,11 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mAvoidCollisionShape.reset(compound.release()); @@ -959,11 +962,11 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) { - mNiTriShape.data = Nif::NiGeometryDataPtr(nullptr); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mData = Nif::NiGeometryDataPtr(nullptr); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -977,12 +980,12 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_triangles_should_return_shape_with_null_collision_shape) { - auto data = static_cast(mNiTriShape.data.getPtr()); - data->triangles.clear(); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + auto data = static_cast(mNiTriShape.mData.getPtr()); + data->mTriangles.clear(); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -994,15 +997,15 @@ namespace } TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + for_root_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) { - mNiStringExtraData.string = "NCC__"; + mNiStringExtraData.mData = "NCC__"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -1010,8 +1013,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1022,16 +1027,16 @@ namespace } TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + for_root_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) { - mNiStringExtraData.next = Nif::ExtraPtr(&mNiStringExtraData2); - mNiStringExtraData2.string = "NCC__"; + mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); + mNiStringExtraData2.mData = "NCC__"; mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; - mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -1039,8 +1044,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1049,16 +1056,16 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + TEST_F( + TestBulletNifLoader, for_root_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) { - mNiStringExtraData.string = "NC___"; + mNiStringExtraData.mData = "NC___"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -1066,8 +1073,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1077,16 +1086,16 @@ namespace } TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + for_root_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) { - mNiStringExtraData.next = Nif::ExtraPtr(&mNiStringExtraData2); - mNiStringExtraData2.string = "NC___"; + mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); + mNiStringExtraData2.mData = "NC___"; mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; - mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -1094,8 +1103,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1104,6 +1115,33 @@ namespace EXPECT_EQ(*result, expected); } + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_should_ignore_extra_data) + { + mNiStringExtraData.mData = "NC___"; + mNiStringExtraData.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file(testNif); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + TEST_F(TestBulletNifLoader, for_empty_root_collision_node_without_nc_should_return_shape_with_cameraonly_collision) { Nif::NiTriShape niTriShape; @@ -1111,16 +1149,16 @@ namespace init(niTriShape); init(emptyCollisionNode); - niTriShape.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); - niTriShape.parents.push_back(&mNiNode); + niTriShape.mData = Nif::NiGeometryDataPtr(&mNiTriShapeData); + niTriShape.mParents.push_back(&mNiNode); emptyCollisionNode.recType = Nif::RC_RootCollisionNode; - emptyCollisionNode.parents.push_back(&mNiNode); + emptyCollisionNode.mParents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList( - std::vector({ Nif::NodePtr(&niTriShape), Nif::NodePtr(&emptyCollisionNode) })); + mNiNode.mChildren + = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&niTriShape), Nif::NiAVObjectPtr(&emptyCollisionNode) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -1128,8 +1166,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1138,38 +1178,19 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_mrk_should_return_shape_with_null_collision_shape) - { - mNiStringExtraData.string = "MRK"; - mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiNode); - file.mHash = mHash; - - const auto result = mLoader.load(file); - - Resource::BulletShape expected; - - EXPECT_EQ(*result, expected); - } - TEST_F(TestBulletNifLoader, bsx_editor_marker_flag_disables_collision_for_markers) { - mNiIntegerExtraData.data = 32; // BSX flag "editor marker" + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mName = "EditorMarker"; + mNiIntegerExtraData.mData = 34; // BSXFlags "has collision" | "editor marker" mNiIntegerExtraData.recType = Nif::RC_BSXFlags; - mNiTriShape.extralist.push_back(Nif::ExtraPtr(&mNiIntegerExtraData)); - mNiTriShape.parents.push_back(&mNiNode); - mNiTriShape.name = "EditorMarker"; - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiNode.mExtraList.push_back(Nif::ExtraPtr(&mNiIntegerExtraData)); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; + file.mVersion = Nif::NIFStream::generateVersion(10, 0, 1, 0); const auto result = mLoader.load(file); @@ -1178,54 +1199,29 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, - for_tri_shape_child_node_with_extra_data_string_mrk_and_other_collision_node_should_return_shape_with_triangle_mesh_shape_with_all_meshes) + TEST_F(TestBulletNifLoader, mrk_editor_marker_flag_disables_collision_for_markers) { - mNiStringExtraData.string = "MRK"; + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mName = "Tri EditorMarker"; + mNiStringExtraData.mData = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; - mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); - mNiTriShape.parents.push_back(&mNiNode2); - mNiNode2.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - mNiNode2.recType = Nif::RC_RootCollisionNode; - mNiNode2.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiNode2) })); - mNiNode.recType = Nif::RC_NiNode; + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; const auto result = mLoader.load(file); - std::unique_ptr triangles(new btTriangleMesh(false)); - triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); - std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); - Resource::BulletShape expected; - expected.mCollisionShape.reset(compound.release()); - - EXPECT_EQ(*result, expected); - } - - TEST_F(TestBulletNifLoader, should_ignore_tri_shape_data_with_mismatching_data_rec_type) - { - mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); - - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiTriShape); - file.mHash = mHash; - - const auto result = mLoader.load(file); - - const Resource::BulletShape expected; EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_tri_strips_root_node_should_return_static_shape) { - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiTriStrips); file.mHash = mHash; @@ -1234,8 +1230,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); triangles->addTriangle(btVector3(1, 0, 0), btVector3(0, 1, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1243,26 +1241,11 @@ namespace EXPECT_EQ(*result, expected); } - TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_mismatching_data_rec_type) - { - mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); - - Nif::NIFFile file("test.nif"); - file.mRoots.push_back(&mNiTriStrips); - file.mHash = mHash; - - const auto result = mLoader.load(file); - - const Resource::BulletShape expected; - - EXPECT_EQ(*result, expected); - } - TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_empty_strips) { - mNiTriStripsData.strips.clear(); + mNiTriStripsData.mStrips.clear(); - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiTriStrips); file.mHash = mHash; @@ -1275,9 +1258,9 @@ namespace TEST_F(TestBulletNifLoader, for_static_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) { - mNiTriStripsData.strips.front() = { 0, 1 }; + mNiTriStripsData.mStrips.front() = { 0, 1 }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiTriStrips); file.mHash = mHash; @@ -1290,12 +1273,12 @@ namespace TEST_F(TestBulletNifLoader, for_avoid_collision_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) { - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; mNiNode.recType = Nif::RC_AvoidNode; - mNiTriStripsData.strips.front() = { 0, 1 }; + mNiTriStripsData.mStrips.front() = { 0, 1 }; - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&mNiTriStrips); file.mHash = mHash; @@ -1308,11 +1291,11 @@ namespace TEST_F(TestBulletNifLoader, for_animated_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) { - mNiTriStripsData.strips.front() = { 0, 1 }; - mNiTriStrips.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriStrips) })); + mNiTriStripsData.mStrips.front() = { 0, 1 }; + mNiTriStrips.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriStrips) }; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mHash = mHash; @@ -1325,11 +1308,11 @@ namespace TEST_F(TestBulletNifLoader, should_not_add_static_mesh_with_no_triangles_to_compound_shape) { - mNiTriStripsData.strips.front() = { 0, 1 }; - mNiTriShape.parents.push_back(&mNiNode); - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); + mNiTriStripsData.mStrips.front() = { 0, 1 }; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mRoots.push_back(&mNiTriStrips); file.mHash = mHash; @@ -1338,9 +1321,10 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); - + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); std::unique_ptr compound(new btCompoundShape); - compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); Resource::BulletShape expected; expected.mCollisionShape.reset(compound.release()); @@ -1351,15 +1335,15 @@ namespace TEST_F(TestBulletNifLoader, should_handle_node_with_multiple_parents) { - copy(mTransform, mNiTriShape.trafo); - mNiTriShape.trafo.scale = 4; - mNiTriShape.parents = { &mNiNode, &mNiNode2 }; - mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - mNiNode.trafo.scale = 2; - mNiNode2.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape) })); - mNiNode2.trafo.scale = 3; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 4; + mNiTriShape.mParents = { &mNiNode, &mNiNode2 }; + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.mTransform.mScale = 2; + mNiNode2.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode2.mTransform.mScale = 3; - Nif::NIFFile file("xtest.nif"); + Nif::NIFFile file(xtestNif); file.mRoots.push_back(&mNiNode); file.mRoots.push_back(&mNiNode2); file.mHash = mHash; @@ -1369,18 +1353,42 @@ namespace std::unique_ptr triangles1(new btTriangleMesh(false)); triangles1->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh1(new Resource::TriangleMeshShape(triangles1.release(), true)); - mesh1->setLocalScaling(btVector3(8, 8, 8)); std::unique_ptr triangles2(new btTriangleMesh(false)); triangles2->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); - mesh2->setLocalScaling(btVector3(12, 12, 12)); std::unique_ptr shape(new btCompoundShape); - shape->addChildShape(mTransformScale2, mesh1.release()); - shape->addChildShape(mTransformScale3, mesh2.release()); + shape->addChildShape( + mTransformScale2, new Resource::ScaledTriangleMeshShape(mesh1.release(), btVector3(8, 8, 8))); + shape->addChildShape( + mTransformScale3, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(12, 12, 12))); Resource::BulletShape expected; expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = { { -1, 0 } }; EXPECT_EQ(*result, expected); } + + TEST_F(TestBulletNifLoader, dont_assign_invalid_bounding_box_extents) + { + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 10; + mNiTriShape.mParents.push_back(&mNiNode); + + mNiTriShape2.mName = "Bounding Box"; + mNiTriShape2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiTriShape2.mBounds.mBox.mExtents = osg::Vec3f(-1, -2, -3); + mNiTriShape2.mParents.push_back(&mNiNode); + + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) }; + + Nif::NIFFile file(testNif); + file.mRoots.push_back(&mNiNode); + + const auto result = mLoader.load(file); + + const bool extentsUnassigned + = std::ranges::all_of(result->mCollisionBox.mExtents._v, [](float extent) { return extent == 0.f; }); + + EXPECT_EQ(extentsUnassigned, true); + } } diff --git a/apps/openmw_test_suite/nifosg/testnifloader.cpp b/apps/components_tests/nifosg/testnifloader.cpp similarity index 66% rename from apps/openmw_test_suite/nifosg/testnifloader.cpp rename to apps/components_tests/nifosg/testnifloader.cpp index 5c9caf4799..6a2cad445b 100644 --- a/apps/openmw_test_suite/nifosg/testnifloader.cpp +++ b/apps/components_tests/nifosg/testnifloader.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -25,10 +26,13 @@ namespace using namespace NifOsg; using namespace Nif::Testing; + constexpr VFS::Path::NormalizedView testNif("test.nif"); + struct BaseNifOsgLoaderTest { VFS::Manager mVfs; - Resource::ImageManager mImageManager{ &mVfs }; + Resource::ImageManager mImageManager{ &mVfs, 0 }; + Resource::BgsmFileManager mMaterialManager{ &mVfs, 0 }; const osgDB::ReaderWriter* mReaderWriter = osgDB::Registry::instance()->getReaderWriterForExtension("osgt"); osg::ref_ptr mOptions = new osgDB::Options; @@ -66,11 +70,11 @@ namespace TEST_F(NifOsgLoaderTest, shouldLoadFileWithDefaultNode) { - Nif::Node node; + Nif::NiAVObject node; init(node); - Nif::NIFFile file("test.nif"); + Nif::NIFFile file(testNif); file.mRoots.push_back(&node); - auto result = Loader::load(file, &mImageManager); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); EXPECT_EQ(serialize(*result), R"( osg::Group { UniqueID 1 @@ -108,7 +112,7 @@ osg::Group { )"); } - std::string formatOsgNodeForShaderProperty(std::string_view shaderPrefix) + std::string formatOsgNodeForBSShaderProperty(std::string_view shaderPrefix) { std::ostringstream oss; oss << R"( @@ -165,6 +169,73 @@ osg::Group { return oss.str(); } + std::string formatOsgNodeForBSLightingShaderProperty(std::string_view shaderPrefix) + { + std::ostringstream oss; + oss << R"( +osg::Group { + UniqueID 1 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 2 + UDC_UserObjects 1 { + osg::StringValueObject { + UniqueID 3 + Name "fileHash" + } + } + } + } + Children 1 { + osg::Group { + UniqueID 4 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 5 + UDC_UserObjects 3 { + osg::UIntValueObject { + UniqueID 6 + Name "recIndex" + Value 4294967295 + } + osg::StringValueObject { + UniqueID 7 + Name "shaderPrefix" + Value ")" + << shaderPrefix << R"(" + } + osg::BoolValueObject { + UniqueID 8 + Name "shaderRequired" + Value TRUE + } + } + } + } + StateSet TRUE { + osg::StateSet { + UniqueID 9 + ModeList 1 { + GL_DEPTH_TEST ON + } + AttributeList 1 { + osg::Depth { + UniqueID 10 + Function LEQUAL + } + Value OFF + } + } + } + } + } +} +)"; + return oss.str(); + } + struct ShaderPrefixParams { unsigned int mShaderType; @@ -183,18 +254,18 @@ osg::Group { TEST_P(NifOsgLoaderBSShaderPrefixTest, shouldAddShaderPrefix) { - Nif::Node node; + Nif::NiAVObject node; init(node); Nif::BSShaderPPLightingProperty property; property.recType = Nif::RC_BSShaderPPLightingProperty; - property.textureSet = nullptr; - property.controller = nullptr; - property.type = GetParam().mShaderType; - node.props.push_back(Nif::RecordPtrT(&property)); - Nif::NIFFile file("test.nif"); + property.mTextureSet = nullptr; + property.mController = nullptr; + property.mType = GetParam().mShaderType; + node.mProperties.push_back(Nif::RecordPtrT(&property)); + Nif::NIFFile file(testNif); file.mRoots.push_back(&node); - auto result = Loader::load(file, &mImageManager); - EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix)); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); + EXPECT_EQ(serialize(*result), formatOsgNodeForBSShaderProperty(GetParam().mExpectedShaderPrefix)); } INSTANTIATE_TEST_SUITE_P(Params, NifOsgLoaderBSShaderPrefixTest, ValuesIn(NifOsgLoaderBSShaderPrefixTest::sParams)); @@ -211,18 +282,20 @@ osg::Group { TEST_P(NifOsgLoaderBSLightingShaderPrefixTest, shouldAddShaderPrefix) { - Nif::Node node; + Nif::NiAVObject node; init(node); Nif::BSLightingShaderProperty property; property.recType = Nif::RC_BSLightingShaderProperty; property.mTextureSet = nullptr; - property.controller = nullptr; - property.type = GetParam().mShaderType; - node.props.push_back(Nif::RecordPtrT(&property)); - Nif::NIFFile file("test.nif"); + property.mController = nullptr; + property.mType = GetParam().mShaderType; + property.mShaderFlags1 |= Nif::BSShaderFlags1::BSSFlag1_DepthTest; + property.mShaderFlags2 |= Nif::BSShaderFlags2::BSSFlag2_DepthWrite; + node.mProperties.push_back(Nif::RecordPtrT(&property)); + Nif::NIFFile file(testNif); file.mRoots.push_back(&node); - auto result = Loader::load(file, &mImageManager); - EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix)); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); + EXPECT_EQ(serialize(*result), formatOsgNodeForBSLightingShaderProperty(GetParam().mExpectedShaderPrefix)); } INSTANTIATE_TEST_SUITE_P( diff --git a/apps/components_tests/resource/testobjectcache.cpp b/apps/components_tests/resource/testobjectcache.cpp new file mode 100644 index 0000000000..e2f5799edb --- /dev/null +++ b/apps/components_tests/resource/testobjectcache.cpp @@ -0,0 +1,377 @@ +#include + +#include +#include + +#include + +namespace Resource +{ + namespace + { + using namespace ::testing; + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldReturnNullptrByDefault) + { + osg::ref_ptr> cache(new GenericObjectCache); + EXPECT_EQ(cache->getRefFromObjectCache(42), nullptr); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldReturnNulloptByDefault) + { + osg::ref_ptr> cache(new GenericObjectCache); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(42), std::nullopt); + } + + struct Object : osg::Object + { + Object() = default; + + Object(const Object& other, const osg::CopyOp& copyOp = osg::CopyOp()) + : osg::Object(other, copyOp) + { + } + + META_Object(ResourceTest, Object) + }; + + TEST(ResourceGenericObjectCacheTest, shouldStoreValues) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_EQ(cache->getRefFromObjectCache(key), value); + } + + TEST(ResourceGenericObjectCacheTest, shouldStoreNullptrValues) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(nullptr)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldExtendLifetimeForItemsWithZeroTimestamp) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value, 0); + value = nullptr; + + const double referenceTime = 1000; + const double expiryDelay = 1; + cache->update(referenceTime, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldReplaceExistingItemByKey) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key = 42; + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(key, value1); + ASSERT_EQ(cache->getRefFromObjectCache(key), value1); + cache->addEntryToObjectCache(key, value2); + EXPECT_EQ(cache->getRefFromObjectCache(key), value2); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldMarkLifetime) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr, referenceTime + expiryDelay); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + 2 * expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldRemoveExpiredItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 1; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + value = nullptr; + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + ASSERT_EQ(cache->getStats().mExpired, 0); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + ASSERT_EQ(cache->getStats().mExpired, 1); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepExternallyReferencedItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 1; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(value)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepNotExpiredItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + value = nullptr; + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepNotExpiredNullptrItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldNotExtendItemLifetime) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + cache->addEntryToObjectCache("a", nullptr); + cache->addEntryToObjectCache("c", nullptr); + EXPECT_THAT(cache->lowerBound(std::string_view("b")), Optional(Pair("c", _))); + } + + TEST(ResourceGenericObjectCacheTest, shouldSupportRemovingItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + ASSERT_EQ(cache->getRefFromObjectCache(key), value); + cache->removeFromObjectCache(key); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, clearShouldRemoveAllItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key1 = 42; + const int key2 = 13; + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(key1, value1); + cache->addEntryToObjectCache(key2, value2); + + ASSERT_EQ(cache->getRefFromObjectCache(key1), value1); + ASSERT_EQ(cache->getRefFromObjectCache(key2), value2); + + cache->clear(); + + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key1), std::nullopt); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key2), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, callShouldIterateOverAllItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(3, value3); + + std::vector> actual; + cache->call([&](int key, osg::Object* value) { actual.emplace_back(key, value); }); + + EXPECT_THAT(actual, ElementsAre(Pair(1, value1.get()), Pair(2, value2.get()), Pair(3, value3.get()))); + } + + TEST(ResourceGenericObjectCacheTest, getStatsShouldReturnNumberOrAddedItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(13, value1); + cache->addEntryToObjectCache(42, value2); + + const CacheStats stats = cache->getStats(); + + EXPECT_EQ(stats.mSize, 2); + } + + TEST(ResourceGenericObjectCacheTest, getStatsShouldReturnNumberOrGetsAndHits) + { + osg::ref_ptr> cache(new GenericObjectCache); + + { + const CacheStats stats = cache->getStats(); + + EXPECT_EQ(stats.mGet, 0); + EXPECT_EQ(stats.mHit, 0); + } + + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(13, value); + cache->getRefFromObjectCache(13); + cache->getRefFromObjectCache(42); + + { + const CacheStats stats = cache->getStats(); + + EXPECT_EQ(stats.mGet, 2); + EXPECT_EQ(stats.mHit, 1); + } + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldReturnFirstNotLessThatGivenKey) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(4, value3); + + EXPECT_THAT(cache->lowerBound(3), Optional(Pair(4, value3))); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldReturnNulloptWhenKeyIsGreaterThanAnyOther) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(3, value3); + + EXPECT_EQ(cache->lowerBound(4), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(std::string_view("key"), value); + EXPECT_EQ(cache->getRefFromObjectCache(key), value); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldKeyMoving) + { + osg::ref_ptr> cache(new GenericObjectCache); + std::string key(128, 'a'); + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(std::move(key), value); + EXPECT_EQ(key, ""); + EXPECT_EQ(cache->getRefFromObjectCache(std::string(128, 'a')), value); + } + + TEST(ResourceGenericObjectCacheTest, removeFromObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + ASSERT_EQ(cache->getRefFromObjectCache(key), value); + cache->removeFromObjectCache(std::string_view("key")); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_EQ(cache->getRefFromObjectCache(std::string_view("key")), value); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(std::string_view("key")), Optional(value)); + } + + TEST(ResourceGenericObjectCacheTest, checkInObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_TRUE(cache->checkInObjectCache(std::string_view("key"), 0)); + } + } +} diff --git a/apps/components_tests/sceneutil/osgacontroller.cpp b/apps/components_tests/sceneutil/osgacontroller.cpp new file mode 100644 index 0000000000..309de4a878 --- /dev/null +++ b/apps/components_tests/sceneutil/osgacontroller.cpp @@ -0,0 +1,131 @@ +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace SceneUtil; + + static const std::string ROOT_BONE_NAME = "bip01"; + + // Creates a merged anim track with a single root channel with two start/end matrix transforms + osg::ref_ptr createMergedAnimationTrack(std::string name, osg::Matrixf startTransform, + osg::Matrixf endTransform, float startTime = 0.0f, float endTime = 1.0f) + { + osg::ref_ptr mergedAnimationTrack = new Resource::Animation; + mergedAnimationTrack->setName(name); + + osgAnimation::MatrixKeyframeContainer* cbCntr = new osgAnimation::MatrixKeyframeContainer; + cbCntr->push_back(osgAnimation::MatrixKeyframe(startTime, startTransform)); + cbCntr->push_back(osgAnimation::MatrixKeyframe(endTime, endTransform)); + + osg::ref_ptr rootChannel = new osgAnimation::MatrixLinearChannel; + rootChannel->setName("transform"); + rootChannel->setTargetName(ROOT_BONE_NAME); + rootChannel->getOrCreateSampler()->setKeyframeContainer(cbCntr); + mergedAnimationTrack->addChannel(rootChannel); + return mergedAnimationTrack; + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnSampledChannelTranslationForBip01) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); // should sample this + emulatedAnimations.push_back({ 1.1f, 2.0f, "test2" }); // should ignore this + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform2 = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + endTransform2.setTrans(2.0f, 2.0f, 2.0f); + controller.addMergedAnimationTrack( + createMergedAnimationTrack("test2", endTransform, endTransform2, 0.1f, 0.9f)); + + // should be halfway between 0,0,0 and 1,1,1 + osg::Vec3f translation = controller.getTranslation(0.5f); + EXPECT_EQ(translation, osg::Vec3f(0.5f, 0.5f, 0.5f)); + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnZeroVectorIfNotFound) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + + // Has no emulated animation at time so will return 0,0,0 + osg::Vec3f translation = controller.getTranslation(100.0f); + EXPECT_EQ(translation, osg::Vec3f(0.0f, 0.0f, 0.0f)); + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnZeroVectorIfNoMergedTracks) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + // Has no merged tracks so will return 0,0,0 + osg::Vec3f translation = controller.getTranslation(0.5); + EXPECT_EQ(translation, osg::Vec3f(0.0f, 0.0f, 0.0f)); + } + + TEST(OsgAnimationControllerTest, getTransformShouldReturnIdentityIfNotFound) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + + // Has no emulated animation at time so will return identity + EXPECT_EQ(controller.getTransformForNode(100.0f, ROOT_BONE_NAME), osg::Matrixf::identity()); + + // Has no bone animation at time so will return identity + EXPECT_EQ(controller.getTransformForNode(0.5f, "wrongbone"), osg::Matrixf::identity()); + } + + TEST(OsgAnimationControllerTest, getTransformShouldReturnSampledAnimMatrixAtTime) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); // should sample this + emulatedAnimations.push_back({ 1.1f, 2.0f, "test2" }); // should ignore this + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + osg::Matrixf endTransform2 = osg::Matrixf::identity(); + endTransform2.setTrans(2.0f, 2.0f, 2.0f); + controller.addMergedAnimationTrack( + createMergedAnimationTrack("test2", endTransform, endTransform2, 0.1f, 0.9f)); + + EXPECT_EQ(controller.getTransformForNode(0.0f, ROOT_BONE_NAME), startTransform); // start of test1 + EXPECT_EQ(controller.getTransformForNode(1.0f, ROOT_BONE_NAME), endTransform); // end of test1 + EXPECT_EQ(controller.getTransformForNode(1.1f, ROOT_BONE_NAME), endTransform); // start of test2 + EXPECT_EQ(controller.getTransformForNode(2.0f, ROOT_BONE_NAME), endTransform2); // end of test2 + } +} diff --git a/apps/openmw_test_suite/serialization/binaryreader.cpp b/apps/components_tests/serialization/binaryreader.cpp similarity index 100% rename from apps/openmw_test_suite/serialization/binaryreader.cpp rename to apps/components_tests/serialization/binaryreader.cpp diff --git a/apps/openmw_test_suite/serialization/binarywriter.cpp b/apps/components_tests/serialization/binarywriter.cpp similarity index 100% rename from apps/openmw_test_suite/serialization/binarywriter.cpp rename to apps/components_tests/serialization/binarywriter.cpp diff --git a/apps/openmw_test_suite/serialization/format.hpp b/apps/components_tests/serialization/format.hpp similarity index 100% rename from apps/openmw_test_suite/serialization/format.hpp rename to apps/components_tests/serialization/format.hpp diff --git a/apps/openmw_test_suite/serialization/integration.cpp b/apps/components_tests/serialization/integration.cpp similarity index 100% rename from apps/openmw_test_suite/serialization/integration.cpp rename to apps/components_tests/serialization/integration.cpp diff --git a/apps/openmw_test_suite/serialization/sizeaccumulator.cpp b/apps/components_tests/serialization/sizeaccumulator.cpp similarity index 100% rename from apps/openmw_test_suite/serialization/sizeaccumulator.cpp rename to apps/components_tests/serialization/sizeaccumulator.cpp diff --git a/apps/openmw_test_suite/settings/parser.cpp b/apps/components_tests/settings/parser.cpp similarity index 99% rename from apps/openmw_test_suite/settings/parser.cpp rename to apps/components_tests/settings/parser.cpp index 3712ca6513..af514dbdd7 100644 --- a/apps/openmw_test_suite/settings/parser.cpp +++ b/apps/components_tests/settings/parser.cpp @@ -1,11 +1,10 @@ #include +#include #include #include -#include "../testing_util.hpp" - namespace { using namespace testing; diff --git a/apps/openmw_test_suite/settings/shadermanager.cpp b/apps/components_tests/settings/shadermanager.cpp similarity index 98% rename from apps/openmw_test_suite/settings/shadermanager.cpp rename to apps/components_tests/settings/shadermanager.cpp index c252e08fc6..c3ba5084c2 100644 --- a/apps/openmw_test_suite/settings/shadermanager.cpp +++ b/apps/components_tests/settings/shadermanager.cpp @@ -1,12 +1,11 @@ #include +#include #include #include #include -#include "../testing_util.hpp" - namespace { using namespace testing; diff --git a/apps/openmw_test_suite/settings/testvalues.cpp b/apps/components_tests/settings/testvalues.cpp similarity index 75% rename from apps/openmw_test_suite/settings/testvalues.cpp rename to apps/components_tests/settings/testvalues.cpp index 81af308795..236417b559 100644 --- a/apps/openmw_test_suite/settings/testvalues.cpp +++ b/apps/components_tests/settings/testvalues.cpp @@ -59,6 +59,33 @@ namespace Settings EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1); } + TEST_F(SettingsValuesTest, constructorWithDefaultShouldDoLookup) + { + Manager::mUserSettings[std::make_pair("category", "value")] = "13"; + Index index; + SettingValue value{ index, "category", "value", 42 }; + EXPECT_EQ(value.get(), 13); + value.reset(); + EXPECT_EQ(value.get(), 42); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldSanitize) + { + Manager::mUserSettings[std::make_pair("category", "value")] = "2"; + Index index; + SettingValue value{ index, "category", "value", -1, Settings::makeClampSanitizerInt(0, 1) }; + EXPECT_EQ(value.get(), 1); + value.reset(); + EXPECT_EQ(value.get(), 0); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldFallbackToDefault) + { + Index index; + const SettingValue value{ index, "category", "value", 42 }; + EXPECT_EQ(value.get(), 42); + } + TEST_F(SettingsValuesTest, moveConstructorShouldSetDefaults) { Index index; @@ -79,6 +106,13 @@ namespace Settings EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1); } + TEST_F(SettingsValuesTest, moveConstructorShouldThrowOnMissingSetting) + { + Index index; + SettingValue defaultValue{ index, "category", "value", 42 }; + EXPECT_THROW([&] { SettingValue value(std::move(defaultValue)); }(), std::runtime_error); + } + TEST_F(SettingsValuesTest, findShouldThrowExceptionOnTypeMismatch) { Index index; diff --git a/apps/openmw_test_suite/shader/parsedefines.cpp b/apps/components_tests/shader/parsedefines.cpp similarity index 100% rename from apps/openmw_test_suite/shader/parsedefines.cpp rename to apps/components_tests/shader/parsedefines.cpp diff --git a/apps/openmw_test_suite/shader/parsefors.cpp b/apps/components_tests/shader/parsefors.cpp similarity index 100% rename from apps/openmw_test_suite/shader/parsefors.cpp rename to apps/components_tests/shader/parsefors.cpp diff --git a/apps/openmw_test_suite/shader/parselinks.cpp b/apps/components_tests/shader/parselinks.cpp similarity index 100% rename from apps/openmw_test_suite/shader/parselinks.cpp rename to apps/components_tests/shader/parselinks.cpp diff --git a/apps/openmw_test_suite/shader/shadermanager.cpp b/apps/components_tests/shader/shadermanager.cpp similarity index 99% rename from apps/openmw_test_suite/shader/shadermanager.cpp rename to apps/components_tests/shader/shadermanager.cpp index 3d7eaecf00..5b11d31a44 100644 --- a/apps/openmw_test_suite/shader/shadermanager.cpp +++ b/apps/components_tests/shader/shadermanager.cpp @@ -1,12 +1,11 @@ #include #include +#include #include #include -#include "../testing_util.hpp" - namespace { using namespace testing; diff --git a/apps/openmw_test_suite/sqlite3/db.cpp b/apps/components_tests/sqlite3/db.cpp similarity index 100% rename from apps/openmw_test_suite/sqlite3/db.cpp rename to apps/components_tests/sqlite3/db.cpp diff --git a/apps/openmw_test_suite/sqlite3/request.cpp b/apps/components_tests/sqlite3/request.cpp similarity index 98% rename from apps/openmw_test_suite/sqlite3/request.cpp rename to apps/components_tests/sqlite3/request.cpp index 23efe9dc2e..c299493952 100644 --- a/apps/openmw_test_suite/sqlite3/request.cpp +++ b/apps/components_tests/sqlite3/request.cpp @@ -151,7 +151,7 @@ namespace const std::int64_t value = 1099511627776; EXPECT_EQ(execute(*mDb, insert, value), 1); Statement select(*mDb, GetAll("ints")); - std::vector> result; + std::vector> result; request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); EXPECT_THAT(result, ElementsAre(std::tuple(value))); } @@ -205,7 +205,7 @@ namespace const std::int64_t value = 1099511627776; EXPECT_EQ(execute(*mDb, insert, value), 1); Statement select(*mDb, GetExact("ints")); - std::vector> result; + std::vector> result; request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); EXPECT_THAT(result, ElementsAre(std::tuple(value))); } diff --git a/apps/openmw_test_suite/sqlite3/statement.cpp b/apps/components_tests/sqlite3/statement.cpp similarity index 100% rename from apps/openmw_test_suite/sqlite3/statement.cpp rename to apps/components_tests/sqlite3/statement.cpp diff --git a/apps/openmw_test_suite/sqlite3/transaction.cpp b/apps/components_tests/sqlite3/transaction.cpp similarity index 100% rename from apps/openmw_test_suite/sqlite3/transaction.cpp rename to apps/components_tests/sqlite3/transaction.cpp diff --git a/apps/openmw_test_suite/toutf8/data/french-utf8.txt b/apps/components_tests/toutf8/data/french-utf8.txt similarity index 100% rename from apps/openmw_test_suite/toutf8/data/french-utf8.txt rename to apps/components_tests/toutf8/data/french-utf8.txt diff --git a/apps/openmw_test_suite/toutf8/data/french-win1252.txt b/apps/components_tests/toutf8/data/french-win1252.txt similarity index 100% rename from apps/openmw_test_suite/toutf8/data/french-win1252.txt rename to apps/components_tests/toutf8/data/french-win1252.txt diff --git a/apps/openmw_test_suite/toutf8/data/russian-utf8.txt b/apps/components_tests/toutf8/data/russian-utf8.txt similarity index 100% rename from apps/openmw_test_suite/toutf8/data/russian-utf8.txt rename to apps/components_tests/toutf8/data/russian-utf8.txt diff --git a/apps/openmw_test_suite/toutf8/data/russian-win1251.txt b/apps/components_tests/toutf8/data/russian-win1251.txt similarity index 100% rename from apps/openmw_test_suite/toutf8/data/russian-win1251.txt rename to apps/components_tests/toutf8/data/russian-win1251.txt diff --git a/apps/openmw_test_suite/toutf8/toutf8.cpp b/apps/components_tests/toutf8/toutf8.cpp similarity index 97% rename from apps/openmw_test_suite/toutf8/toutf8.cpp rename to apps/components_tests/toutf8/toutf8.cpp index f189294cf2..704ee6742d 100644 --- a/apps/openmw_test_suite/toutf8/toutf8.cpp +++ b/apps/components_tests/toutf8/toutf8.cpp @@ -26,7 +26,7 @@ namespace { std::ifstream file; file.exceptions(std::ios::failbit | std::ios::badbit); - file.open(std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "apps" / "openmw_test_suite" / "toutf8" / "data" + file.open(std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "apps" / "components_tests" / "toutf8" / "data" / Misc::StringUtils::stringToU8String(fileName)); std::stringstream buffer; buffer << file.rdbuf(); @@ -47,7 +47,7 @@ namespace { std::string input; for (int c = 1; c <= std::numeric_limits::max(); ++c) - input.push_back(c); + input.push_back(static_cast(c)); Utf8Encoder encoder(FromType::CP437); const std::string_view result = encoder.getUtf8(input); EXPECT_EQ(result.data(), input.data()); @@ -99,7 +99,7 @@ namespace { std::string input; for (int c = 1; c <= std::numeric_limits::max(); ++c) - input.push_back(c); + input.push_back(static_cast(c)); Utf8Encoder encoder(FromType::CP437); const std::string_view result = encoder.getLegacyEnc(input); EXPECT_EQ(result.data(), input.data()); diff --git a/apps/components_tests/vfs/testpathutil.cpp b/apps/components_tests/vfs/testpathutil.cpp new file mode 100644 index 0000000000..3819f9905a --- /dev/null +++ b/apps/components_tests/vfs/testpathutil.cpp @@ -0,0 +1,201 @@ +#include + +#include + +#include + +namespace VFS::Path +{ + namespace + { + using namespace testing; + + TEST(NormalizedTest, shouldSupportDefaultConstructor) + { + const Normalized value; + EXPECT_EQ(value.value(), ""); + } + + TEST(NormalizedTest, shouldSupportConstructorFromString) + { + const std::string string("Foo\\Bar/baz"); + const Normalized value(string); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportConstructorFromConstCharPtr) + { + const char* const ptr = "Foo\\Bar/baz"; + const Normalized value(ptr); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportConstructorFromStringView) + { + const std::string_view view = "Foo\\Bar/baz"; + const Normalized value(view); + EXPECT_EQ(value.view(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportConstructorFromNormalizedView) + { + const NormalizedView view("foo/bar/baz"); + const Normalized value(view); + EXPECT_EQ(value.view(), "foo/bar/baz"); + } + + TEST(NormalizedTest, supportMovingValueOut) + { + Normalized value("Foo\\Bar/baz"); + EXPECT_EQ(std::move(value).value(), "foo/bar/baz"); + EXPECT_EQ(value.value(), ""); + } + + TEST(NormalizedTest, isNotEqualToNotNormalized) + { + const Normalized value("Foo\\Bar/baz"); + EXPECT_NE(value.value(), "Foo\\Bar/baz"); + } + + TEST(NormalizedTest, shouldSupportOperatorLeftShiftToOStream) + { + const Normalized value("Foo\\Bar/baz"); + std::stringstream stream; + stream << value; + EXPECT_EQ(stream.str(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportOperatorDivEqual) + { + Normalized value("foo/bar"); + value /= NormalizedView("baz"); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportOperatorDivEqualWithStringView) + { + Normalized value("foo/bar"); + value /= std::string_view("BAZ"); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, changeExtensionShouldReplaceAfterLastDot) + { + Normalized value("foo/bar.a"); + ASSERT_TRUE(value.changeExtension("so")); + EXPECT_EQ(value.value(), "foo/bar.so"); + } + + TEST(NormalizedTest, changeExtensionShouldNormalizeExtension) + { + Normalized value("foo/bar.a"); + ASSERT_TRUE(value.changeExtension("SO")); + EXPECT_EQ(value.value(), "foo/bar.so"); + } + + TEST(NormalizedTest, changeExtensionShouldIgnorePathWithoutADot) + { + Normalized value("foo/bar"); + ASSERT_FALSE(value.changeExtension("so")); + EXPECT_EQ(value.value(), "foo/bar"); + } + + TEST(NormalizedTest, changeExtensionShouldIgnorePathWithDotBeforeSeparator) + { + Normalized value("foo.bar/baz"); + ASSERT_FALSE(value.changeExtension("so")); + EXPECT_EQ(value.value(), "foo.bar/baz"); + } + + TEST(NormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithDot) + { + Normalized value("foo.a"); + EXPECT_THROW(value.changeExtension(".so"), std::invalid_argument); + } + + TEST(NormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithSeparator) + { + Normalized value("foo.a"); + EXPECT_THROW(value.changeExtension("/so"), std::invalid_argument); + } + + template + struct NormalizedOperatorsTest : Test + { + }; + + TYPED_TEST_SUITE_P(NormalizedOperatorsTest); + + TYPED_TEST_P(NormalizedOperatorsTest, supportsEqual) + { + using Type0 = typename TypeParam::Type0; + using Type1 = typename TypeParam::Type1; + const Type0 normalized{ "a/foo/bar/baz" }; + const Type1 otherEqual{ "a/foo/bar/baz" }; + const Type1 otherNotEqual{ "b/foo/bar/baz" }; + EXPECT_EQ(normalized, otherEqual); + EXPECT_EQ(otherEqual, normalized); + EXPECT_NE(normalized, otherNotEqual); + EXPECT_NE(otherNotEqual, normalized); + } + + TYPED_TEST_P(NormalizedOperatorsTest, supportsLess) + { + using Type0 = typename TypeParam::Type0; + using Type1 = typename TypeParam::Type1; + const Type0 normalized{ "b/foo/bar/baz" }; + const Type1 otherEqual{ "b/foo/bar/baz" }; + const Type1 otherLess{ "a/foo/bar/baz" }; + const Type1 otherGreater{ "c/foo/bar/baz" }; + EXPECT_FALSE(normalized < otherEqual); + EXPECT_FALSE(otherEqual < normalized); + EXPECT_LT(otherLess, normalized); + EXPECT_FALSE(normalized < otherLess); + EXPECT_LT(normalized, otherGreater); + EXPECT_FALSE(otherGreater < normalized); + } + + REGISTER_TYPED_TEST_SUITE_P(NormalizedOperatorsTest, supportsEqual, supportsLess); + + template + struct TypePair + { + using Type0 = T0; + using Type1 = T1; + }; + + using TypePairs = Types, TypePair, + TypePair, TypePair, + TypePair, TypePair, + TypePair, TypePair, + TypePair, TypePair>; + + INSTANTIATE_TYPED_TEST_SUITE_P(Typed, NormalizedOperatorsTest, TypePairs); + + TEST(NormalizedViewTest, shouldSupportConstructorFromNormalized) + { + const Normalized value("Foo\\Bar/baz"); + const NormalizedView view(value); + EXPECT_EQ(view.value(), "foo/bar/baz"); + } + + TEST(NormalizedViewTest, shouldSupportConstexprConstructorFromNormalizedStringLiteral) + { + constexpr NormalizedView view("foo/bar/baz"); + EXPECT_EQ(view.value(), "foo/bar/baz"); + } + + TEST(NormalizedViewTest, constructorShouldThrowExceptionOnNotNormalized) + { + EXPECT_THROW([] { NormalizedView("Foo\\Bar/baz"); }(), std::invalid_argument); + } + + TEST(NormalizedView, shouldSupportOperatorDiv) + { + const NormalizedView a("foo/bar"); + const NormalizedView b("baz"); + const Normalized result = a / b; + EXPECT_EQ(result.value(), "foo/bar/baz"); + } + } +} diff --git a/apps/esmtool/CMakeLists.txt b/apps/esmtool/CMakeLists.txt index 6f7fa1a993..6dd592a4fe 100644 --- a/apps/esmtool/CMakeLists.txt +++ b/apps/esmtool/CMakeLists.txt @@ -16,7 +16,7 @@ openmw_add_executable(esmtool ) target_link_libraries(esmtool - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options components ) @@ -25,7 +25,7 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(esmtool gcov) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(esmtool PRIVATE diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 092be66e97..0473676f93 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -69,7 +69,7 @@ Allowed options)"); addOption("name,n", bpo::value(), "Show only the record with this name. Only affects dump mode."); addOption("plain,p", "Print contents of dialogs, books and scripts. " - "(skipped by default)" + "(skipped by default) " "Only affects dump mode."); addOption("quiet,q", "Suppress all record information. Useful for speed tests."); addOption("loadcells,C", "Browse through contents of all cells."); @@ -156,7 +156,7 @@ Allowed options)"); return false; }*/ - const auto inputFiles = variables["input-file"].as(); + const auto& inputFiles = variables["input-file"].as(); info.filename = inputFiles[0].u8string(); // This call to u8string is redundant, but required to build on // MSVC 14.26 due to implementation bugs. if (inputFiles.size() > 1) @@ -265,7 +265,7 @@ namespace std::cout << " Faction rank: " << ref.mFactionRank << '\n'; std::cout << " Enchantment charge: " << ref.mEnchantmentCharge << '\n'; std::cout << " Uses/health: " << ref.mChargeInt << '\n'; - std::cout << " Gold value: " << ref.mGoldValue << '\n'; + std::cout << " Count: " << ref.mCount << '\n'; std::cout << " Blocked: " << static_cast(ref.mReferenceBlocked) << '\n'; std::cout << " Deleted: " << deleted << '\n'; if (!ref.mKey.empty()) @@ -341,7 +341,7 @@ namespace { std::cout << "Author: " << esm.getAuthor() << '\n' << "Description: " << esm.getDesc() << '\n' - << "File format version: " << esm.getFVer() << '\n'; + << "File format version: " << esm.esmVersionF() << '\n'; std::vector masterData = esm.getGameFiles(); if (!masterData.empty()) { @@ -390,7 +390,7 @@ namespace if (!quiet && interested) { - std::cout << "\nRecord: " << n.toStringView() << " '" << record->getId() << "'\n" + std::cout << "\nRecord: " << n.toStringView() << " " << record->getId() << "\n" << "Record flags: " << recordFlags(record->getFlags()) << '\n'; record->print(); } @@ -508,7 +508,7 @@ namespace ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); esm.setEncoder(&encoder); esm.setHeader(data.mHeader); - esm.setVersion(ESM::VER_13); + esm.setVersion(ESM::VER_130); esm.setRecordCount(recordCount); std::fstream save(info.outname, std::fstream::out | std::fstream::binary); diff --git a/apps/esmtool/labels.cpp b/apps/esmtool/labels.cpp index d0a443de53..3d64563923 100644 --- a/apps/esmtool/labels.cpp +++ b/apps/esmtool/labels.cpp @@ -1,10 +1,13 @@ #include "labels.hpp" +#include +#include #include #include #include #include #include +#include #include #include #include @@ -571,13 +574,14 @@ std::string_view enchantTypeLabel(int idx) std::string_view ruleFunction(int idx) { - if (idx >= 0 && idx <= 72) + if (idx >= ESM::DialogueCondition::Function_FacReactionLowest + && idx <= ESM::DialogueCondition::Function_PcWerewolfKills) { static constexpr std::string_view ruleFunctions[] = { - "Reaction Low", - "Reaction High", + "Lowest Faction Reaction", + "Highest Faction Reaction", "Rank Requirement", - "NPC? Reputation", + "NPC Reputation", "Health Percent", "Player Reputation", "NPC Level", @@ -647,6 +651,7 @@ std::string_view ruleFunction(int idx) "Flee", "Should Attack", "Werewolf", + "Werewolf Kills", }; return ruleFunctions[idx]; } @@ -762,18 +767,16 @@ std::string enchantmentFlags(int flags) std::string landFlags(std::uint32_t flags) { std::string properties; - // The ESM component says that this first four bits are used, but - // only the first three bits are used as far as I can tell. - // There's also no enumeration of the bit in the ESM component. if (flags == 0) properties += "[None] "; - if (flags & 0x00000001) - properties += "Unknown1 "; - if (flags & 0x00000004) - properties += "Unknown3 "; - if (flags & 0x00000002) - properties += "Unknown2 "; - if (flags & 0xFFFFFFF8) + if (flags & ESM::Land::Flag_HeightsNormals) + properties += "HeightsNormals "; + if (flags & ESM::Land::Flag_Colors) + properties += "Colors "; + if (flags & ESM::Land::Flag_Textures) + properties += "Textures "; + int unused = 0xFFFFFFFF ^ (ESM::Land::Flag_HeightsNormals | ESM::Land::Flag_Colors | ESM::Land::Flag_Textures); + if (flags & unused) properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; @@ -987,3 +990,16 @@ std::string recordFlags(uint32_t flags) properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } + +std::string potionFlags(int flags) +{ + std::string properties; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Potion::Autocalc) + properties += "Autocalc "; + if (flags & (0xFFFFFFFF ^ ESM::Enchantment::Autocalc)) + properties += "Invalid "; + properties += Misc::StringUtils::format("(0x%08X)", flags); + return properties; +} diff --git a/apps/esmtool/labels.hpp b/apps/esmtool/labels.hpp index df6d419ca3..c3a78141b4 100644 --- a/apps/esmtool/labels.hpp +++ b/apps/esmtool/labels.hpp @@ -60,6 +60,7 @@ std::string itemListFlags(int flags); std::string lightFlags(int flags); std::string magicEffectFlags(int flags); std::string npcFlags(int flags); +std::string potionFlags(int flags); std::string raceFlags(int flags); std::string spellFlags(int flags); std::string weaponFlags(int flags); diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index 0c22114749..b9c64d964a 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -2,6 +2,7 @@ #include "labels.hpp" #include +#include #include #include @@ -57,112 +58,82 @@ namespace std::cout << " Cell Name: " << p.mCellName << std::endl; } - std::string ruleString(const ESM::DialInfo::SelectStruct& ss) + std::string ruleString(const ESM::DialogueCondition& ss) { - std::string rule = ss.mSelectRule; + std::string_view type_str = "INVALID"; + std::string_view func_str; - if (rule.length() < 5) - return "INVALID"; - - char type = rule[1]; - char indicator = rule[2]; - - std::string type_str = "INVALID"; - std::string func_str = Misc::StringUtils::format("INVALID=%s", rule.substr(1, 3)); - int func = Misc::StringUtils::toNumeric(rule.substr(2, 2), 0); - - switch (type) + switch (ss.mFunction) { - case '1': - type_str = "Function"; - func_str = std::string(ruleFunction(func)); + case ESM::DialogueCondition::Function_Global: + type_str = "Global"; + func_str = ss.mVariable; break; - case '2': - if (indicator == 's') - type_str = "Global short"; - else if (indicator == 'l') - type_str = "Global long"; - else if (indicator == 'f') - type_str = "Global float"; + case ESM::DialogueCondition::Function_Local: + type_str = "Local"; + func_str = ss.mVariable; break; - case '3': - if (indicator == 's') - type_str = "Local short"; - else if (indicator == 'l') - type_str = "Local long"; - else if (indicator == 'f') - type_str = "Local float"; + case ESM::DialogueCondition::Function_Journal: + type_str = "Journal"; + func_str = ss.mVariable; break; - case '4': - if (indicator == 'J') - type_str = "Journal"; + case ESM::DialogueCondition::Function_Item: + type_str = "Item count"; + func_str = ss.mVariable; break; - case '5': - if (indicator == 'I') - type_str = "Item type"; + case ESM::DialogueCondition::Function_Dead: + type_str = "Dead"; + func_str = ss.mVariable; break; - case '6': - if (indicator == 'D') - type_str = "NPC Dead"; + case ESM::DialogueCondition::Function_NotId: + type_str = "Not ID"; + func_str = ss.mVariable; break; - case '7': - if (indicator == 'X') - type_str = "Not ID"; + case ESM::DialogueCondition::Function_NotFaction: + type_str = "Not Faction"; + func_str = ss.mVariable; break; - case '8': - if (indicator == 'F') - type_str = "Not Faction"; + case ESM::DialogueCondition::Function_NotClass: + type_str = "Not Class"; + func_str = ss.mVariable; break; - case '9': - if (indicator == 'C') - type_str = "Not Class"; + case ESM::DialogueCondition::Function_NotRace: + type_str = "Not Race"; + func_str = ss.mVariable; break; - case 'A': - if (indicator == 'R') - type_str = "Not Race"; + case ESM::DialogueCondition::Function_NotCell: + type_str = "Not Cell"; + func_str = ss.mVariable; break; - case 'B': - if (indicator == 'L') - type_str = "Not Cell"; - break; - case 'C': - if (indicator == 's') - type_str = "Not Local"; + case ESM::DialogueCondition::Function_NotLocal: + type_str = "Not Local"; + func_str = ss.mVariable; break; default: + type_str = "Function"; + func_str = ruleFunction(ss.mFunction); break; } - // Append the variable name to the function string if any. - if (type != '1') - func_str = rule.substr(5); - - // In the previous switch, we assumed that the second char was X - // for all types not qual to one. If this wasn't true, go back to - // the error message. - if (type != '1' && rule[3] != 'X') - func_str = Misc::StringUtils::format("INVALID=%s", rule.substr(1, 3)); - - char oper = rule[4]; - std::string oper_str = "??"; - switch (oper) + std::string_view oper_str = "??"; + switch (ss.mComparison) { - case '0': + case ESM::DialogueCondition::Comp_Eq: oper_str = "=="; break; - case '1': + case ESM::DialogueCondition::Comp_Ne: oper_str = "!="; break; - case '2': + case ESM::DialogueCondition::Comp_Gt: oper_str = "> "; break; - case '3': + case ESM::DialogueCondition::Comp_Ge: oper_str = ">="; break; - case '4': + case ESM::DialogueCondition::Comp_Ls: oper_str = "< "; break; - case '5': + case ESM::DialogueCondition::Comp_Le: oper_str = "<="; break; default: @@ -170,7 +141,7 @@ namespace } std::ostringstream stream; - stream << ss.mValue; + std::visit([&](auto value) { stream << value; }, ss.mValue); std::string result = Misc::StringUtils::format("%-12s %-32s %2s %s", type_str, func_str, oper_str, stream.str()); @@ -180,22 +151,23 @@ namespace void printEffectList(const ESM::EffectList& effects) { int i = 0; - for (const ESM::ENAMstruct& effect : effects.mList) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mEffectID) << " (" << effect.mEffectID - << ")" << std::endl; - if (effect.mSkill != -1) - std::cout << " Skill: " << skillLabel(effect.mSkill) << " (" << (int)effect.mSkill << ")" + std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mData.mEffectID) << " (" + << effect.mData.mEffectID << ")" << std::endl; + if (effect.mData.mSkill != -1) + std::cout << " Skill: " << skillLabel(effect.mData.mSkill) << " (" << (int)effect.mData.mSkill << ")" << std::endl; - if (effect.mAttribute != -1) - std::cout << " Attribute: " << attributeLabel(effect.mAttribute) << " (" << (int)effect.mAttribute - << ")" << std::endl; - std::cout << " Range: " << rangeTypeLabel(effect.mRange) << " (" << effect.mRange << ")" << std::endl; + if (effect.mData.mAttribute != -1) + std::cout << " Attribute: " << attributeLabel(effect.mData.mAttribute) << " (" + << (int)effect.mData.mAttribute << ")" << std::endl; + std::cout << " Range: " << rangeTypeLabel(effect.mData.mRange) << " (" << effect.mData.mRange << ")" + << std::endl; // Area is always zero if range type is "Self" - if (effect.mRange != ESM::RT_Self) - std::cout << " Area: " << effect.mArea << std::endl; - std::cout << " Duration: " << effect.mDuration << std::endl; - std::cout << " Magnitude: " << effect.mMagnMin << "-" << effect.mMagnMax << std::endl; + if (effect.mData.mRange != ESM::RT_Self) + std::cout << " Area: " << effect.mData.mArea << std::endl; + std::cout << " Duration: " << effect.mData.mDuration << std::endl; + std::cout << " Magnitude: " << effect.mData.mMagnMin << "-" << effect.mData.mMagnMax << std::endl; i++; } } @@ -464,7 +436,8 @@ namespace EsmTool { std::cout << " Name: " << mData.mName << std::endl; std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; std::cout << " Deleted: " << mIsDeleted << std::endl; } @@ -478,7 +451,7 @@ namespace EsmTool std::cout << " Script: " << mData.mScript << std::endl; std::cout << " Weight: " << mData.mData.mWeight << std::endl; std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " AutoCalc: " << mData.mData.mAutoCalc << std::endl; + std::cout << " Flags: " << potionFlags(mData.mData.mFlags) << std::endl; printEffectList(mData.mEffects); std::cout << " Deleted: " << mIsDeleted << std::endl; } @@ -516,7 +489,8 @@ namespace EsmTool std::cout << " Name: " << mData.mName << std::endl; std::cout << " Model: " << mData.mModel << std::endl; std::cout << " Icon: " << mData.mIcon << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; std::cout << " Type: " << apparatusTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" << std::endl; std::cout << " Weight: " << mData.mData.mWeight << std::endl; @@ -610,7 +584,6 @@ namespace EsmTool } else std::cout << " Map Color: " << Misc::StringUtils::format("0x%08X", mData.mMapColor) << std::endl; - std::cout << " Water Level Int: " << mData.mWaterInt << std::endl; std::cout << " RefId counter: " << mData.mRefNumCounter << std::endl; std::cout << " Deleted: " << mIsDeleted << std::endl; } @@ -679,7 +652,8 @@ namespace EsmTool { std::cout << " Name: " << mData.mName << std::endl; std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; std::cout << " Flags: " << creatureFlags((int)mData.mFlags) << std::endl; std::cout << " Blood Type: " << mData.mBloodType + 1 << std::endl; std::cout << " Original: " << mData.mOriginal << std::endl; @@ -690,14 +664,8 @@ namespace EsmTool std::cout << " Level: " << mData.mData.mLevel << std::endl; std::cout << " Attributes:" << std::endl; - std::cout << " Strength: " << mData.mData.mStrength << std::endl; - std::cout << " Intelligence: " << mData.mData.mIntelligence << std::endl; - std::cout << " Willpower: " << mData.mData.mWillpower << std::endl; - std::cout << " Agility: " << mData.mData.mAgility << std::endl; - std::cout << " Speed: " << mData.mData.mSpeed << std::endl; - std::cout << " Endurance: " << mData.mData.mEndurance << std::endl; - std::cout << " Personality: " << mData.mData.mPersonality << std::endl; - std::cout << " Luck: " << mData.mData.mLuck << std::endl; + for (size_t i = 0; i < mData.mData.mAttributes.size(); ++i) + std::cout << " " << ESM::Attribute::indexToRefId(i) << ": " << mData.mData.mAttributes[i] << std::endl; std::cout << " Health: " << mData.mData.mHealth << std::endl; std::cout << " Magicka: " << mData.mData.mMana << std::endl; @@ -725,9 +693,6 @@ namespace EsmTool std::cout << " AI Fight:" << (int)mData.mAiData.mFight << std::endl; std::cout << " AI Flee:" << (int)mData.mAiData.mFlee << std::endl; std::cout << " AI Alarm:" << (int)mData.mAiData.mAlarm << std::endl; - std::cout << " AI U1:" << (int)mData.mAiData.mU1 << std::endl; - std::cout << " AI U2:" << (int)mData.mAiData.mU2 << std::endl; - std::cout << " AI U3:" << (int)mData.mAiData.mU3 << std::endl; std::cout << " AI Services:" << Misc::StringUtils::format("0x%08X", mData.mAiData.mServices) << std::endl; for (const ESM::AIPackage& package : mData.mAiPackage.mList) @@ -753,7 +718,8 @@ namespace EsmTool { std::cout << " Name: " << mData.mName << std::endl; std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; std::cout << " OpenSound: " << mData.mOpenSound << std::endl; std::cout << " CloseSound: " << mData.mCloseSound << std::endl; std::cout << " Deleted: " << mIsDeleted << std::endl; @@ -845,10 +811,9 @@ namespace EsmTool std::cout << " Quest Status: " << questStatusLabel(mData.mQuestStatus) << " (" << mData.mQuestStatus << ")" << std::endl; - std::cout << " Unknown1: " << mData.mData.mUnknown1 << std::endl; - std::cout << " Unknown2: " << (int)mData.mData.mUnknown2 << std::endl; + std::cout << " Type: " << dialogTypeLabel(mData.mData.mType) << std::endl; - for (const ESM::DialInfo::SelectStruct& rule : mData.mSelects) + for (const auto& rule : mData.mSelects) std::cout << " Select Rule: " << ruleString(rule) << std::endl; if (!mData.mResultScript.empty()) @@ -902,10 +867,9 @@ namespace EsmTool if (const ESM::Land::LandData* data = mData.getLandData(mData.mDataTypes)) { - std::cout << " Height Offset: " << data->mHeightOffset << std::endl; - // Lots of missing members. - std::cout << " Unknown1: " << data->mUnk1 << std::endl; - std::cout << " Unknown2: " << static_cast(data->mUnk2) << std::endl; + std::cout << " MinHeight: " << data->mMinHeight << std::endl; + std::cout << " MaxHeight: " << data->mMaxHeight << std::endl; + std::cout << " DataLoaded: " << data->mDataLoaded << std::endl; } mData.unloadData(); std::cout << " Deleted: " << mIsDeleted << std::endl; @@ -1090,14 +1054,8 @@ namespace EsmTool std::cout << " Rank: " << (int)mData.mNpdt.mRank << std::endl; std::cout << " Attributes:" << std::endl; - std::cout << " Strength: " << (int)mData.mNpdt.mStrength << std::endl; - std::cout << " Intelligence: " << (int)mData.mNpdt.mIntelligence << std::endl; - std::cout << " Willpower: " << (int)mData.mNpdt.mWillpower << std::endl; - std::cout << " Agility: " << (int)mData.mNpdt.mAgility << std::endl; - std::cout << " Speed: " << (int)mData.mNpdt.mSpeed << std::endl; - std::cout << " Endurance: " << (int)mData.mNpdt.mEndurance << std::endl; - std::cout << " Personality: " << (int)mData.mNpdt.mPersonality << std::endl; - std::cout << " Luck: " << (int)mData.mNpdt.mLuck << std::endl; + for (size_t i = 0; i != mData.mNpdt.mAttributes.size(); i++) + std::cout << " " << attributeLabel(i) << ": " << int(mData.mNpdt.mAttributes[i]) << std::endl; std::cout << " Skills:" << std::endl; for (size_t i = 0; i != mData.mNpdt.mSkills.size(); i++) @@ -1123,9 +1081,6 @@ namespace EsmTool std::cout << " AI Fight:" << (int)mData.mAiData.mFight << std::endl; std::cout << " AI Flee:" << (int)mData.mAiData.mFlee << std::endl; std::cout << " AI Alarm:" << (int)mData.mAiData.mAlarm << std::endl; - std::cout << " AI U1:" << (int)mData.mAiData.mU1 << std::endl; - std::cout << " AI U2:" << (int)mData.mAiData.mU2 << std::endl; - std::cout << " AI U3:" << (int)mData.mAiData.mU3 << std::endl; std::cout << " AI Services:" << Misc::StringUtils::format("0x%08X", mData.mAiData.mServices) << std::endl; for (const ESM::AIPackage& package : mData.mAiPackage.mList) @@ -1139,9 +1094,9 @@ namespace EsmTool { std::cout << " Cell: " << mData.mCell << std::endl; std::cout << " Coordinates: (" << mData.mData.mX << "," << mData.mData.mY << ")" << std::endl; - std::cout << " Unknown S1: " << mData.mData.mS1 << std::endl; - if (static_cast(mData.mData.mS2) != mData.mPoints.size()) - std::cout << " Reported Point Count: " << mData.mData.mS2 << std::endl; + std::cout << " Granularity: " << mData.mData.mGranularity << std::endl; + if (mData.mData.mPoints != mData.mPoints.size()) + std::cout << " Reported Point Count: " << mData.mData.mPoints << std::endl; std::cout << " Point Count: " << mData.mPoints.size() << std::endl; std::cout << " Edge Count: " << mData.mEdges.size() << std::endl; @@ -1152,7 +1107,6 @@ namespace EsmTool std::cout << " Coordinates: (" << point.mX << "," << point.mY << "," << point.mZ << ")" << std::endl; std::cout << " Auto-Generated: " << (int)point.mAutogenerated << std::endl; std::cout << " Connections: " << (int)point.mConnectionNum << std::endl; - std::cout << " Unknown: " << point.mUnknown << std::endl; i++; } @@ -1160,7 +1114,7 @@ namespace EsmTool for (const ESM::Pathgrid::Edge& edge : mData.mEdges) { std::cout << " Edge[" << i << "]: " << edge.mV0 << " -> " << edge.mV1 << std::endl; - if (edge.mV0 >= static_cast(mData.mData.mS2) || edge.mV1 >= static_cast(mData.mData.mS2)) + if (edge.mV0 >= mData.mData.mPoints || edge.mV1 >= mData.mData.mPoints) std::cout << " BAD POINT IN EDGE!" << std::endl; i++; } @@ -1175,19 +1129,23 @@ namespace EsmTool std::cout << " Description: " << mData.mDescription << std::endl; std::cout << " Flags: " << raceFlags(mData.mData.mFlags) << std::endl; - for (int i = 0; i < 2; ++i) + std::cout << " Male:" << std::endl; + for (int j = 0; j < ESM::Attribute::Length; ++j) { - bool male = i == 0; - - std::cout << (male ? " Male:" : " Female:") << std::endl; - - for (int j = 0; j < ESM::Attribute::Length; ++j) - std::cout << " " << ESM::Attribute::indexToRefId(j) << ": " - << mData.mData.mAttributeValues[j].getValue(male) << std::endl; - - std::cout << " Height: " << mData.mData.mHeight.getValue(male) << std::endl; - std::cout << " Weight: " << mData.mData.mWeight.getValue(male) << std::endl; + ESM::RefId id = ESM::Attribute::indexToRefId(j); + std::cout << " " << id << ": " << mData.mData.getAttribute(id, true) << std::endl; } + std::cout << " Height: " << mData.mData.mMaleHeight << std::endl; + std::cout << " Weight: " << mData.mData.mMaleWeight << std::endl; + + std::cout << " Female:" << std::endl; + for (int j = 0; j < ESM::Attribute::Length; ++j) + { + ESM::RefId id = ESM::Attribute::indexToRefId(j); + std::cout << " " << id << ": " << mData.mData.getAttribute(id, false) << std::endl; + } + std::cout << " Height: " << mData.mData.mFemaleHeight << std::endl; + std::cout << " Weight: " << mData.mData.mFemaleWeight << std::endl; for (const auto& bonus : mData.mData.mBonus) // Not all races have 7 skills. @@ -1207,16 +1165,11 @@ namespace EsmTool std::cout << " Name: " << mData.mName << std::endl; std::cout << " Weather:" << std::endl; - std::cout << " Clear: " << (int)mData.mData.mClear << std::endl; - std::cout << " Cloudy: " << (int)mData.mData.mCloudy << std::endl; - std::cout << " Foggy: " << (int)mData.mData.mFoggy << std::endl; - std::cout << " Overcast: " << (int)mData.mData.mOvercast << std::endl; - std::cout << " Rain: " << (int)mData.mData.mOvercast << std::endl; - std::cout << " Thunder: " << (int)mData.mData.mThunder << std::endl; - std::cout << " Ash: " << (int)mData.mData.mAsh << std::endl; - std::cout << " Blight: " << (int)mData.mData.mBlight << std::endl; - std::cout << " Snow: " << (int)mData.mData.mSnow << std::endl; - std::cout << " Blizzard: " << (int)mData.mData.mBlizzard << std::endl; + std::array weathers + = { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" }; + for (size_t i = 0; i < weathers.size(); ++i) + std::cout << " " << weathers[i] << ": " << static_cast(mData.mData.mProbabilities[i]) + << std::endl; std::cout << " Map Color: " << mData.mMapColor << std::endl; if (!mData.mSleepList.empty()) std::cout << " Sleep List: " << mData.mSleepList << std::endl; @@ -1229,11 +1182,11 @@ namespace EsmTool { std::cout << " Name: " << mData.mId << std::endl; - std::cout << " Num Shorts: " << mData.mData.mNumShorts << std::endl; - std::cout << " Num Longs: " << mData.mData.mNumLongs << std::endl; - std::cout << " Num Floats: " << mData.mData.mNumFloats << std::endl; - std::cout << " Script Data Size: " << mData.mData.mScriptDataSize << std::endl; - std::cout << " Table Size: " << mData.mData.mStringTableSize << std::endl; + std::cout << " Num Shorts: " << mData.mNumShorts << std::endl; + std::cout << " Num Longs: " << mData.mNumLongs << std::endl; + std::cout << " Num Floats: " << mData.mNumFloats << std::endl; + std::cout << " Script Data Size: " << mData.mScriptData.size() << std::endl; + std::cout << " Table Size: " << ESM::computeScriptStringTableSize(mData.mVarNames) << std::endl; for (const std::string& variable : mData.mVarNames) std::cout << " Variable: " << variable << std::endl; @@ -1351,28 +1304,26 @@ namespace EsmTool template <> void Record::print() { - std::cout << " Id:" << std::endl; - std::cout << " CellId: " << mData.mCellState.mId << std::endl; - std::cout << " Index:" << std::endl; - std::cout << " WaterLevel: " << mData.mCellState.mWaterLevel << std::endl; - std::cout << " HasFogOfWar: " << mData.mCellState.mHasFogOfWar << std::endl; - std::cout << " LastRespawn:" << std::endl; + std::cout << " Cell Id: \"" << mData.mCellState.mId.toString() << "\"" << std::endl; + std::cout << " Water Level: " << mData.mCellState.mWaterLevel << std::endl; + std::cout << " Has Fog Of War: " << mData.mCellState.mHasFogOfWar << std::endl; + std::cout << " Last Respawn:" << std::endl; std::cout << " Day:" << mData.mCellState.mLastRespawn.mDay << std::endl; std::cout << " Hour:" << mData.mCellState.mLastRespawn.mHour << std::endl; if (mData.mCellState.mHasFogOfWar) { - std::cout << " NorthMarkerAngle: " << mData.mFogState.mNorthMarkerAngle << std::endl; + std::cout << " North Marker Angle: " << mData.mFogState.mNorthMarkerAngle << std::endl; std::cout << " Bounds:" << std::endl; - std::cout << " MinX: " << mData.mFogState.mBounds.mMinX << std::endl; - std::cout << " MinY: " << mData.mFogState.mBounds.mMinY << std::endl; - std::cout << " MaxX: " << mData.mFogState.mBounds.mMaxX << std::endl; - std::cout << " MaxY: " << mData.mFogState.mBounds.mMaxY << std::endl; + std::cout << " Min X: " << mData.mFogState.mBounds.mMinX << std::endl; + std::cout << " Min Y: " << mData.mFogState.mBounds.mMinY << std::endl; + std::cout << " Max X: " << mData.mFogState.mBounds.mMaxX << std::endl; + std::cout << " Max Y: " << mData.mFogState.mBounds.mMaxY << std::endl; for (const ESM::FogTexture& fogTexture : mData.mFogState.mFogTextures) { - std::cout << " FogTexture:" << std::endl; + std::cout << " Fog Texture:" << std::endl; std::cout << " X: " << fogTexture.mX << std::endl; std::cout << " Y: " << fogTexture.mY << std::endl; - std::cout << " ImageData: (" << fogTexture.mImageData.size() << ")" << std::endl; + std::cout << " Image Data: (" << fogTexture.mImageData.size() << ")" << std::endl; } } } @@ -1380,7 +1331,7 @@ namespace EsmTool template <> std::string Record::getId() const { - return mData.mName; + return std::string(); // No ID for Cell record } template <> @@ -1410,9 +1361,7 @@ namespace EsmTool template <> std::string Record::getId() const { - std::ostringstream stream; - stream << mData.mCellState.mId; - return stream.str(); + return std::string(); // No ID for CellState record } } // end namespace diff --git a/apps/esmtool/tes4.cpp b/apps/esmtool/tes4.cpp index 6791694a7c..5b657da573 100644 --- a/apps/esmtool/tes4.cpp +++ b/apps/esmtool/tes4.cpp @@ -562,7 +562,7 @@ namespace EsmTool { std::cout << "Author: " << reader.getAuthor() << '\n' << "Description: " << reader.getDesc() << '\n' - << "File format version: " << reader.esmVersion() << '\n'; + << "File format version: " << reader.esmVersionF() << '\n'; if (const std::vector& masterData = reader.getGameFiles(); !masterData.empty()) { diff --git a/apps/essimporter/CMakeLists.txt b/apps/essimporter/CMakeLists.txt index c6c98791e3..217d3e7b50 100644 --- a/apps/essimporter/CMakeLists.txt +++ b/apps/essimporter/CMakeLists.txt @@ -34,7 +34,7 @@ openmw_add_executable(openmw-essimporter ) target_link_libraries(openmw-essimporter - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options components ) @@ -47,7 +47,7 @@ if (WIN32) INSTALL(TARGETS openmw-essimporter RUNTIME DESTINATION ".") endif(WIN32) -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-essimporter PRIVATE diff --git a/apps/essimporter/convertacdt.cpp b/apps/essimporter/convertacdt.cpp index 8342310cf6..a737e0a3a2 100644 --- a/apps/essimporter/convertacdt.cpp +++ b/apps/essimporter/convertacdt.cpp @@ -85,7 +85,7 @@ namespace ESSImport Misc::StringUtils::lowerCaseInPlace(group); ESM::AnimationState::ScriptedAnimation scriptedAnim; - scriptedAnim.mGroup = group; + scriptedAnim.mGroup = std::move(group); scriptedAnim.mTime = anis.mTime; scriptedAnim.mAbsolute = true; // Neither loop count nor queueing seems to be supported by the ess format. diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index 4751fd9497..ebb0c9d281 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -1,6 +1,7 @@ #include "converter.hpp" #include +#include #include #include @@ -33,7 +34,7 @@ namespace objstate.mPosition = cellref.mPos; objstate.mRef.mRefNum = cellref.mRefNum; if (cellref.mDeleted) - objstate.mCount = 0; + objstate.mRef.mCount = 0; convertSCRI(cellref.mActorData.mSCRI, objstate.mLocals); objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); @@ -90,14 +91,14 @@ namespace ESSImport struct MAPH { - unsigned int size; - unsigned int value; + uint32_t size; + uint32_t value; }; void ConvertFMAP::read(ESM::ESMReader& esm) { MAPH maph; - esm.getHNTSized<8>(maph, "MAPH"); + esm.getHNT("MAPH", maph.size, maph.value); std::vector data; esm.getSubNameIs("MAPD"); esm.getSubHeader(); @@ -231,7 +232,7 @@ namespace ESSImport esm.skip(4); } - esm.getExact(nam8, 32); + esm.getT(nam8); newcell.mFogOfWar.reserve(16 * 16); for (int x = 0; x < 16; ++x) @@ -278,7 +279,7 @@ namespace ESSImport while (esm.isNextSub("MPCD")) { float notepos[3]; - esm.getHTSized<3 * sizeof(float)>(notepos); + esm.getHT(notepos); // Markers seem to be arranged in a 32*32 grid, notepos has grid-indices. // This seems to be the reason markers can't be placed everywhere in interior cells, @@ -305,12 +306,12 @@ namespace ESSImport mMarkers.push_back(marker); } - newcell.mRefs = cellrefs; + newcell.mRefs = std::move(cellrefs); if (cell.isExterior()) - mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = newcell; + mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = std::move(newcell); else - mIntCells[cell.mName] = newcell; + mIntCells[cell.mName] = std::move(newcell); } void ConvertCell::writeCell(const Cell& cell, ESM::ESMWriter& esm) diff --git a/apps/essimporter/convertinventory.cpp b/apps/essimporter/convertinventory.cpp index 2f03cfaf41..7025b0ae43 100644 --- a/apps/essimporter/convertinventory.cpp +++ b/apps/essimporter/convertinventory.cpp @@ -9,15 +9,14 @@ namespace ESSImport void convertInventory(const Inventory& inventory, ESM::InventoryState& state) { - int index = 0; + uint32_t index = 0; for (const auto& item : inventory.mItems) { ESM::ObjectState objstate; objstate.blank(); objstate.mRef = item; objstate.mRef.mRefID = ESM::RefId::stringRefId(item.mId); - objstate.mCount = std::abs(item.mCount); // restocking items have negative count in the savefile - // openmw handles them differently, so no need to set any flags + objstate.mRef.mCount = item.mCount; state.mItems.push_back(objstate); if (item.mRelativeEquipmentSlot != -1) // Note we should really write the absolute slot here, which we do not know about diff --git a/apps/essimporter/importacdt.hpp b/apps/essimporter/importacdt.hpp index 785e988200..65519c6a6c 100644 --- a/apps/essimporter/importacdt.hpp +++ b/apps/essimporter/importacdt.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_ACDT_H #define OPENMW_ESSIMPORT_ACDT_H +#include #include #include "importscri.hpp" @@ -25,14 +26,12 @@ namespace ESSImport }; /// Actor data, shared by (at least) REFR and CellRef -#pragma pack(push) -#pragma pack(1) struct ACDT { // Note, not stored at *all*: // - Level changes are lost on reload, except for the player (there it's in the NPC record). unsigned char mUnknown[12]; - unsigned int mFlags; + uint32_t mFlags; float mBreathMeter; // Seconds left before drowning unsigned char mUnknown2[20]; float mDynamic[3][2]; @@ -41,7 +40,7 @@ namespace ESSImport float mMagicEffects[27]; // Effect attributes: // https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attributes unsigned char mUnknown4[4]; - unsigned int mGoldPool; + uint32_t mGoldPool; unsigned char mCountDown; // seen the same value as in ACSC.mCorpseClearCountdown, maybe // this one is for respawning? unsigned char mUnknown5[3]; @@ -60,7 +59,6 @@ namespace ESSImport unsigned char mUnknown[3]; float mTime; }; -#pragma pack(pop) struct ActorData { diff --git a/apps/essimporter/importcellref.cpp b/apps/essimporter/importcellref.cpp index a900440a96..9e8e9a6948 100644 --- a/apps/essimporter/importcellref.cpp +++ b/apps/essimporter/importcellref.cpp @@ -1,9 +1,30 @@ #include "importcellref.hpp" #include +#include + +#include namespace ESSImport { + template T> + void decompose(T&& v, const auto& f) + { + f(v.mUnknown, v.mFlags, v.mBreathMeter, v.mUnknown2, v.mDynamic, v.mUnknown3, v.mAttributes, v.mMagicEffects, + v.mUnknown4, v.mGoldPool, v.mCountDown, v.mUnknown5); + } + + template T> + void decompose(T&& v, const auto& f) + { + f(v.mUnknown1, v.mFlags, v.mUnknown2, v.mCorpseClearCountdown, v.mUnknown3); + } + + template T> + void decompose(T&& v, const auto& f) + { + f(v.mGroupIndex, v.mUnknown, v.mTime); + } void CellRef::load(ESM::ESMReader& esm) { @@ -44,19 +65,9 @@ namespace ESSImport bool isDeleted = false; ESM::CellRef::loadData(esm, isDeleted); - mActorData.mHasACDT = false; - if (esm.isNextSub("ACDT")) - { - mActorData.mHasACDT = true; - esm.getHTSized<264>(mActorData.mACDT); - } + mActorData.mHasACDT = esm.getOptionalComposite("ACDT", mActorData.mACDT); - mActorData.mHasACSC = false; - if (esm.isNextSub("ACSC")) - { - mActorData.mHasACSC = true; - esm.getHTSized<112>(mActorData.mACSC); - } + mActorData.mHasACSC = esm.getOptionalComposite("ACSC", mActorData.mACSC); if (esm.isNextSub("ACSL")) esm.skipHSubSize(112); @@ -122,23 +133,16 @@ namespace ESSImport } // FIXME: not all actors have this, add flag - if (esm.isNextSub("CHRD")) // npc only - esm.getHExact(mActorData.mSkills, 27 * 2 * sizeof(int)); + esm.getHNOT("CHRD", mActorData.mSkills); // npc only - if (esm.isNextSub("CRED")) // creature only - esm.getHExact(mActorData.mCombatStats, 3 * 2 * sizeof(int)); + esm.getHNOT("CRED", mActorData.mCombatStats); // creature only mActorData.mSCRI.load(esm); if (esm.isNextSub("ND3D")) esm.skipHSub(); - mActorData.mHasANIS = false; - if (esm.isNextSub("ANIS")) - { - mActorData.mHasANIS = true; - esm.getHTSized<8>(mActorData.mANIS); - } + mActorData.mHasANIS = esm.getOptionalComposite("ANIS", mActorData.mANIS); if (esm.isNextSub("LVCR")) { @@ -155,13 +159,13 @@ namespace ESSImport // DATA should occur for all references, except levelled creature spawners // I've seen DATA *twice* on a creature record, and with the exact same content too! weird // alarmvoi0000.ess - esm.getHNOTSized<24>(mPos, "DATA"); - esm.getHNOTSized<24>(mPos, "DATA"); + for (int i = 0; i < 2; ++i) + esm.getOptionalComposite("DATA", mPos); mDeleted = 0; if (esm.isNextSub("DELE")) { - unsigned int deleted; + uint32_t deleted; esm.getHT(deleted); mDeleted = ((deleted >> 24) & 0x2) != 0; // the other 3 bytes seem to be uninitialized garbage } diff --git a/apps/essimporter/importcntc.cpp b/apps/essimporter/importcntc.cpp index 41f4e50101..34c99babef 100644 --- a/apps/essimporter/importcntc.cpp +++ b/apps/essimporter/importcntc.cpp @@ -1,6 +1,7 @@ #include "importcntc.hpp" #include +#include namespace ESSImport { diff --git a/apps/essimporter/importcntc.hpp b/apps/essimporter/importcntc.hpp index 1bc7d94bd5..6ee843805e 100644 --- a/apps/essimporter/importcntc.hpp +++ b/apps/essimporter/importcntc.hpp @@ -14,7 +14,7 @@ namespace ESSImport /// Changed container contents struct CNTC { - int mIndex; + int32_t mIndex; Inventory mInventory; diff --git a/apps/essimporter/importcrec.hpp b/apps/essimporter/importcrec.hpp index 77933eafe8..5217f4edc4 100644 --- a/apps/essimporter/importcrec.hpp +++ b/apps/essimporter/importcrec.hpp @@ -3,6 +3,7 @@ #include "importinventory.hpp" #include +#include namespace ESM { @@ -15,7 +16,7 @@ namespace ESSImport /// Creature changes struct CREC { - int mIndex; + int32_t mIndex; Inventory mInventory; ESM::AIPackageList mAiPackages; diff --git a/apps/essimporter/importdial.cpp b/apps/essimporter/importdial.cpp index 6c45f9d059..43905738a1 100644 --- a/apps/essimporter/importdial.cpp +++ b/apps/essimporter/importdial.cpp @@ -8,11 +8,11 @@ namespace ESSImport void DIAL::load(ESM::ESMReader& esm) { // See ESM::Dialogue::Type enum, not sure why we would need this here though - int type = 0; + int32_t type = 0; esm.getHNOT(type, "DATA"); // Deleted dialogue in a savefile. No clue what this means... - int deleted = 0; + int32_t deleted = 0; esm.getHNOT(deleted, "DELE"); mIndex = 0; diff --git a/apps/essimporter/importdial.hpp b/apps/essimporter/importdial.hpp index 9a1e882332..b8b6fd536a 100644 --- a/apps/essimporter/importdial.hpp +++ b/apps/essimporter/importdial.hpp @@ -1,5 +1,8 @@ #ifndef OPENMW_ESSIMPORT_IMPORTDIAL_H #define OPENMW_ESSIMPORT_IMPORTDIAL_H + +#include + namespace ESM { class ESMReader; @@ -10,7 +13,7 @@ namespace ESSImport struct DIAL { - int mIndex; // Journal index + int32_t mIndex; // Journal index void load(ESM::ESMReader& esm); }; diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 76b685c8a3..5cc9a8259b 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -135,7 +135,7 @@ namespace ESSImport sub.mFileOffset = esm.getFileOffset(); sub.mName = esm.retSubName().toString(); sub.mData.resize(esm.getSubSize()); - esm.getExact(&sub.mData[0], sub.mData.size()); + esm.getExact(sub.mData.data(), sub.mData.size()); rec.mSubrecords.push_back(sub); } file.mRecords.push_back(rec); diff --git a/apps/essimporter/importgame.cpp b/apps/essimporter/importgame.cpp index 5295d2a1e8..8161a20031 100644 --- a/apps/essimporter/importgame.cpp +++ b/apps/essimporter/importgame.cpp @@ -9,17 +9,17 @@ namespace ESSImport { esm.getSubNameIs("GMDT"); esm.getSubHeader(); - if (esm.getSubSize() == 92) - { - esm.getExact(&mGMDT, 92); - mGMDT.mSecundaPhase = 0; - } - else if (esm.getSubSize() == 96) - { - esm.getTSized<96>(mGMDT); - } - else - esm.fail("unexpected subrecord size for GAME.GMDT"); + bool hasSecundaPhase = esm.getSubSize() == 96; + esm.getT(mGMDT.mCellName); + esm.getT(mGMDT.mFogColour); + esm.getT(mGMDT.mFogDensity); + esm.getT(mGMDT.mCurrentWeather); + esm.getT(mGMDT.mNextWeather); + esm.getT(mGMDT.mWeatherTransition); + esm.getT(mGMDT.mTimeOfNextTransition); + esm.getT(mGMDT.mMasserPhase); + if (hasSecundaPhase) + esm.getT(mGMDT.mSecundaPhase); mGMDT.mWeatherTransition &= (0x000000ff); mGMDT.mSecundaPhase &= (0x000000ff); diff --git a/apps/essimporter/importgame.hpp b/apps/essimporter/importgame.hpp index 8b26b9d8bd..276060ae4c 100644 --- a/apps/essimporter/importgame.hpp +++ b/apps/essimporter/importgame.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_ESSIMPORT_GAME_H #define OPENMW_ESSIMPORT_GAME_H +#include + namespace ESM { class ESMReader; @@ -15,12 +17,12 @@ namespace ESSImport struct GMDT { char mCellName[64]{}; - int mFogColour{ 0 }; + int32_t mFogColour{ 0 }; float mFogDensity{ 0.f }; - int mCurrentWeather{ 0 }, mNextWeather{ 0 }; - int mWeatherTransition{ 0 }; // 0-100 transition between weathers, top 3 bytes may be garbage + int32_t mCurrentWeather{ 0 }, mNextWeather{ 0 }; + int32_t mWeatherTransition{ 0 }; // 0-100 transition between weathers, top 3 bytes may be garbage float mTimeOfNextTransition{ 0.f }; // weather changes when gamehour == timeOfNextTransition - int mMasserPhase{ 0 }, mSecundaPhase{ 0 }; // top 3 bytes may be garbage + int32_t mMasserPhase{ 0 }, mSecundaPhase{ 0 }; // top 3 bytes may be garbage }; GMDT mGMDT; diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp index 9d71c04f2a..f1db301bd0 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -12,7 +12,7 @@ namespace ESSImport while (esm.isNextSub("NPCO")) { ContItem contItem; - esm.getHTSized<36>(contItem); + esm.getHT(contItem.mCount, contItem.mItem.mData); InventoryItem item; item.mId = contItem.mItem.toString(); @@ -28,7 +28,7 @@ namespace ESSImport bool newStack = esm.isNextSub("XIDX"); if (newStack) { - unsigned int idx; + uint32_t idx; esm.getHT(idx); separateStacks = true; item.mCount = 1; @@ -40,7 +40,7 @@ namespace ESSImport bool isDeleted = false; item.ESM::CellRef::loadData(esm, isDeleted); - int charge = -1; + int32_t charge = -1; esm.getHNOT(charge, "XHLT"); item.mChargeInt = charge; @@ -60,7 +60,7 @@ namespace ESSImport // this is currently not handled properly. esm.getSubHeader(); - int itemIndex; // index of the item in the NPCO list + int32_t itemIndex; // index of the item in the NPCO list esm.getT(itemIndex); if (itemIndex < 0 || itemIndex >= int(mItems.size())) @@ -68,7 +68,7 @@ namespace ESSImport // appears to be a relative index for only the *possible* slots this item can be equipped in, // i.e. 0 most of the time - int slotIndex; + int32_t slotIndex; esm.getT(slotIndex); mItems[itemIndex].mRelativeEquipmentSlot = slotIndex; diff --git a/apps/essimporter/importinventory.hpp b/apps/essimporter/importinventory.hpp index 7a11b3f0a0..7261e64f68 100644 --- a/apps/essimporter/importinventory.hpp +++ b/apps/essimporter/importinventory.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_IMPORTINVENTORY_H #define OPENMW_ESSIMPORT_IMPORTINVENTORY_H +#include #include #include @@ -19,7 +20,7 @@ namespace ESSImport struct ContItem { - int mCount; + int32_t mCount; ESM::NAME32 mItem; }; @@ -28,8 +29,8 @@ namespace ESSImport struct InventoryItem : public ESM::CellRef { std::string mId; - int mCount; - int mRelativeEquipmentSlot; + int32_t mCount; + int32_t mRelativeEquipmentSlot; SCRI mSCRI; }; std::vector mItems; diff --git a/apps/essimporter/importklst.cpp b/apps/essimporter/importklst.cpp index d4cfc7f769..2d5e09e913 100644 --- a/apps/essimporter/importklst.cpp +++ b/apps/essimporter/importklst.cpp @@ -10,7 +10,7 @@ namespace ESSImport while (esm.isNextSub("KNAM")) { std::string refId = esm.getHString(); - int count; + int32_t count; esm.getHNT(count, "CNAM"); mKillCounter[refId] = count; } diff --git a/apps/essimporter/importklst.hpp b/apps/essimporter/importklst.hpp index 7c1ff03bb6..9cdb2d701b 100644 --- a/apps/essimporter/importklst.hpp +++ b/apps/essimporter/importklst.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_KLST_H #define OPENMW_ESSIMPORT_KLST_H +#include #include #include @@ -18,9 +19,9 @@ namespace ESSImport void load(ESM::ESMReader& esm); /// RefId, kill count - std::map mKillCounter; + std::map mKillCounter; - int mWerewolfKills; + int32_t mWerewolfKills; }; } diff --git a/apps/essimporter/importnpcc.cpp b/apps/essimporter/importnpcc.cpp index c115040074..c1a53b6cef 100644 --- a/apps/essimporter/importnpcc.cpp +++ b/apps/essimporter/importnpcc.cpp @@ -7,7 +7,7 @@ namespace ESSImport void NPCC::load(ESM::ESMReader& esm) { - esm.getHNTSized<8>(mNPDT, "NPDT"); + esm.getHNT("NPDT", mNPDT.mDisposition, mNPDT.unknown, mNPDT.mReputation, mNPDT.unknown2, mNPDT.mIndex); while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") || esm.isNextSub("AI_A")) diff --git a/apps/essimporter/importnpcc.hpp b/apps/essimporter/importnpcc.hpp index 762add1906..47925226e4 100644 --- a/apps/essimporter/importnpcc.hpp +++ b/apps/essimporter/importnpcc.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESSIMPORT_NPCC_H #include +#include #include "importinventory.hpp" @@ -21,7 +22,7 @@ namespace ESSImport unsigned char unknown; unsigned char mReputation; unsigned char unknown2; - int mIndex; + int32_t mIndex; } mNPDT; Inventory mInventory; diff --git a/apps/essimporter/importplayer.cpp b/apps/essimporter/importplayer.cpp index 165926d15a..f4c280541d 100644 --- a/apps/essimporter/importplayer.cpp +++ b/apps/essimporter/importplayer.cpp @@ -19,7 +19,12 @@ namespace ESSImport mMNAM = esm.getHString(); } - esm.getHNTSized<212>(mPNAM, "PNAM"); + esm.getHNT("PNAM", mPNAM.mPlayerFlags, mPNAM.mLevelProgress, mPNAM.mSkillProgress, mPNAM.mSkillIncreases, + mPNAM.mTelekinesisRangeBonus, mPNAM.mVisionBonus, mPNAM.mDetectKeyMagnitude, + mPNAM.mDetectEnchantmentMagnitude, mPNAM.mDetectAnimalMagnitude, mPNAM.mMarkLocation.mX, + mPNAM.mMarkLocation.mY, mPNAM.mMarkLocation.mZ, mPNAM.mMarkLocation.mRotZ, mPNAM.mMarkLocation.mCellX, + mPNAM.mMarkLocation.mCellY, mPNAM.mUnknown3, mPNAM.mVerticalRotation.mData, mPNAM.mSpecIncreases, + mPNAM.mUnknown4); if (esm.isNextSub("SNAM")) esm.skipHSub(); @@ -50,12 +55,7 @@ namespace ESSImport if (esm.isNextSub("NAM3")) esm.skipHSub(); - mHasENAM = false; - if (esm.isNextSub("ENAM")) - { - mHasENAM = true; - esm.getHTSized<8>(mENAM); - } + mHasENAM = esm.getHNOT("ENAM", mENAM.mCellX, mENAM.mCellY); if (esm.isNextSub("LNAM")) esm.skipHSub(); @@ -63,16 +63,12 @@ namespace ESSImport while (esm.isNextSub("FNAM")) { FNAM fnam; - esm.getHTSized<44>(fnam); + esm.getHT( + fnam.mRank, fnam.mUnknown1, fnam.mReputation, fnam.mFlags, fnam.mUnknown2, fnam.mFactionName.mData); mFactions.push_back(fnam); } - mHasAADT = false; - if (esm.isNextSub("AADT")) // Attack animation data? - { - mHasAADT = true; - esm.getHTSized<44>(mAADT); - } + mHasAADT = esm.getHNOT("AADT", mAADT.animGroupIndex, mAADT.mUnknown5); // Attack animation data? if (esm.isNextSub("KNAM")) esm.skipHSub(); // assigned Quick Keys, I think diff --git a/apps/essimporter/importplayer.hpp b/apps/essimporter/importplayer.hpp index 0fb820cb64..89957bf4b4 100644 --- a/apps/essimporter/importplayer.hpp +++ b/apps/essimporter/importplayer.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESSIMPORT_PLAYER_H #define OPENMW_ESSIMPORT_PLAYER_H +#include #include #include @@ -17,7 +18,7 @@ namespace ESSImport /// Other player data struct PCDT { - int mBounty; + int32_t mBounty; std::string mBirthsign; std::vector mKnownDialogueTopics; @@ -41,13 +42,11 @@ namespace ESSImport PlayerFlags_LevitationDisabled = 0x80000 }; -#pragma pack(push) -#pragma pack(1) struct FNAM { unsigned char mRank; unsigned char mUnknown1[3]; - int mReputation; + int32_t mReputation; unsigned char mFlags; // 0x1: unknown, 0x2: expelled unsigned char mUnknown2[3]; ESM::NAME32 mFactionName; @@ -59,7 +58,7 @@ namespace ESSImport { float mX, mY, mZ; // worldspace position float mRotZ; // Z angle in radians - int mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) + int32_t mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) }; struct Rotation @@ -67,15 +66,15 @@ namespace ESSImport float mData[3][3]; }; - int mPlayerFlags; // controls, camera and draw state - unsigned int mLevelProgress; + int32_t mPlayerFlags; // controls, camera and draw state + uint32_t mLevelProgress; float mSkillProgress[27]; // skill progress, non-uniform scaled unsigned char mSkillIncreases[8]; // number of skill increases for each attribute - int mTelekinesisRangeBonus; // in units; seems redundant + int32_t mTelekinesisRangeBonus; // in units; seems redundant float mVisionBonus; // range: <0.0, 1.0>; affected by light spells and Get/Mod/SetPCVisionBonus - int mDetectKeyMagnitude; // seems redundant - int mDetectEnchantmentMagnitude; // seems redundant - int mDetectAnimalMagnitude; // seems redundant + int32_t mDetectKeyMagnitude; // seems redundant + int32_t mDetectEnchantmentMagnitude; // seems redundant + int32_t mDetectAnimalMagnitude; // seems redundant MarkLocation mMarkLocation; unsigned char mUnknown3[4]; Rotation mVerticalRotation; @@ -85,16 +84,15 @@ namespace ESSImport struct ENAM { - int mCellX; - int mCellY; + int32_t mCellX; + int32_t mCellY; }; struct AADT // 44 bytes { - int animGroupIndex; // See convertANIS() for the mapping. + int32_t animGroupIndex; // See convertANIS() for the mapping. unsigned char mUnknown5[40]; }; -#pragma pack(pop) std::vector mFactions; PNAM mPNAM; diff --git a/apps/essimporter/importproj.cpp b/apps/essimporter/importproj.cpp index f9a92095e0..a09ade81dd 100644 --- a/apps/essimporter/importproj.cpp +++ b/apps/essimporter/importproj.cpp @@ -10,7 +10,9 @@ namespace ESSImport while (esm.isNextSub("PNAM")) { PNAM pnam; - esm.getHTSized<184>(pnam); + esm.getHT(pnam.mAttackStrength, pnam.mSpeed, pnam.mUnknown, pnam.mFlightTime, pnam.mSplmIndex, + pnam.mUnknown2, pnam.mVelocity.mValues, pnam.mPosition.mValues, pnam.mUnknown3, pnam.mActorId.mData, + pnam.mArrowId.mData, pnam.mBowId.mData); mProjectiles.push_back(pnam); } } diff --git a/apps/essimporter/importproj.h b/apps/essimporter/importproj.h index d1c544f66f..04c7b4003e 100644 --- a/apps/essimporter/importproj.h +++ b/apps/essimporter/importproj.h @@ -2,7 +2,9 @@ #define OPENMW_ESSIMPORT_IMPORTPROJ_H #include -#include +#include + +#include #include namespace ESM @@ -16,15 +18,13 @@ namespace ESSImport struct PROJ { -#pragma pack(push) -#pragma pack(1) struct PNAM // 184 bytes { float mAttackStrength; float mSpeed; unsigned char mUnknown[4 * 2]; float mFlightTime; - int mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) + int32_t mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) unsigned char mUnknown2[4]; ESM::Vector3 mVelocity; ESM::Vector3 mPosition; @@ -35,7 +35,6 @@ namespace ESSImport bool isMagic() const { return mSplmIndex != 0; } }; -#pragma pack(pop) std::vector mProjectiles; diff --git a/apps/essimporter/importscpt.cpp b/apps/essimporter/importscpt.cpp index 746d0b90e7..bb62c61103 100644 --- a/apps/essimporter/importscpt.cpp +++ b/apps/essimporter/importscpt.cpp @@ -7,7 +7,8 @@ namespace ESSImport void SCPT::load(ESM::ESMReader& esm) { - esm.getHNTSized<52>(mSCHD, "SCHD"); + esm.getHNT("SCHD", mSCHD.mName.mData, mSCHD.mNumShorts, mSCHD.mNumLongs, mSCHD.mNumFloats, + mSCHD.mScriptDataSize, mSCHD.mStringTableSize); mSCRI.load(esm); diff --git a/apps/essimporter/importscpt.hpp b/apps/essimporter/importscpt.hpp index 8f60532447..7c728ee97e 100644 --- a/apps/essimporter/importscpt.hpp +++ b/apps/essimporter/importscpt.hpp @@ -3,6 +3,8 @@ #include "importscri.hpp" +#include + #include #include @@ -17,7 +19,11 @@ namespace ESSImport struct SCHD { ESM::NAME32 mName; - ESM::Script::SCHDstruct mData; + std::uint32_t mNumShorts; + std::uint32_t mNumLongs; + std::uint32_t mNumFloats; + std::uint32_t mScriptDataSize; + std::uint32_t mStringTableSize; }; // A running global script @@ -29,7 +35,7 @@ namespace ESSImport SCRI mSCRI; bool mRunning; - int mRefNum; // Targeted reference, -1: no reference + int32_t mRefNum; // Targeted reference, -1: no reference void load(ESM::ESMReader& esm); }; diff --git a/apps/essimporter/importscri.cpp b/apps/essimporter/importscri.cpp index b6c1d4094c..c0425cef32 100644 --- a/apps/essimporter/importscri.cpp +++ b/apps/essimporter/importscri.cpp @@ -9,7 +9,7 @@ namespace ESSImport { mScript = esm.getHNOString("SCRI"); - int numShorts = 0, numLongs = 0, numFloats = 0; + int32_t numShorts = 0, numLongs = 0, numFloats = 0; if (esm.isNextSub("SLCS")) { esm.getSubHeader(); @@ -23,7 +23,7 @@ namespace ESSImport esm.getSubHeader(); for (int i = 0; i < numShorts; ++i) { - short val; + int16_t val; esm.getT(val); mShorts.push_back(val); } @@ -35,7 +35,7 @@ namespace ESSImport esm.getSubHeader(); for (int i = 0; i < numLongs; ++i) { - int val; + int32_t val; esm.getT(val); mLongs.push_back(val); } diff --git a/apps/essimporter/importscri.hpp b/apps/essimporter/importscri.hpp index 73d8942f81..0c83a4d3be 100644 --- a/apps/essimporter/importscri.hpp +++ b/apps/essimporter/importscri.hpp @@ -3,6 +3,7 @@ #include +#include #include namespace ESM diff --git a/apps/essimporter/importsplm.cpp b/apps/essimporter/importsplm.cpp index a0478f4d92..6019183f83 100644 --- a/apps/essimporter/importsplm.cpp +++ b/apps/essimporter/importsplm.cpp @@ -11,13 +11,15 @@ namespace ESSImport { ActiveSpell spell; esm.getHT(spell.mIndex); - esm.getHNTSized<160>(spell.mSPDT, "SPDT"); + esm.getHNT("SPDT", spell.mSPDT.mType, spell.mSPDT.mId.mData, spell.mSPDT.mUnknown, + spell.mSPDT.mCasterId.mData, spell.mSPDT.mSourceId.mData, spell.mSPDT.mUnknown2); spell.mTarget = esm.getHNOString("TNAM"); while (esm.isNextSub("NPDT")) { ActiveEffect effect; - esm.getHTSized<56>(effect.mNPDT); + esm.getHT(effect.mNPDT.mAffectedActorId.mData, effect.mNPDT.mUnknown, effect.mNPDT.mMagnitude, + effect.mNPDT.mSecondsActive, effect.mNPDT.mUnknown2); // Effect-specific subrecords can follow: // - INAM for disintegration and bound effects diff --git a/apps/essimporter/importsplm.h b/apps/essimporter/importsplm.h index 8187afb131..762e32d9da 100644 --- a/apps/essimporter/importsplm.h +++ b/apps/essimporter/importsplm.h @@ -2,6 +2,7 @@ #define OPENMW_ESSIMPORT_IMPORTSPLM_H #include +#include #include namespace ESM @@ -15,11 +16,9 @@ namespace ESSImport struct SPLM { -#pragma pack(push) -#pragma pack(1) struct SPDT // 160 bytes { - int mType; // 1 = spell, 2 = enchantment, 3 = potion + int32_t mType; // 1 = spell, 2 = enchantment, 3 = potion ESM::NAME32 mId; // base ID of a spell/enchantment/potion unsigned char mUnknown[4 * 4]; ESM::NAME32 mCasterId; @@ -31,31 +30,29 @@ namespace ESSImport { ESM::NAME32 mAffectedActorId; unsigned char mUnknown[4 * 2]; - int mMagnitude; + int32_t mMagnitude; float mSecondsActive; unsigned char mUnknown2[4 * 2]; }; struct INAM // 40 bytes { - int mUnknown; + int32_t mUnknown; unsigned char mUnknown2; ESM::FixedString<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration }; struct CNAM // 36 bytes { - int mUnknown; // seems to always be 0 + int32_t mUnknown; // seems to always be 0 ESM::NAME32 mSummonedOrCommandedActor[32]; }; struct VNAM // 4 bytes { - int mUnknown; + int32_t mUnknown; }; -#pragma pack(pop) - struct ActiveEffect { NPDT mNPDT; @@ -63,7 +60,7 @@ namespace ESSImport struct ActiveSpell { - int mIndex; + int32_t mIndex; SPDT mSPDT; std::string mTarget; std::vector mActiveEffects; diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index 7d3ad10bb1..f0833e9d81 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -42,8 +42,8 @@ Allowed options)"); Files::ConfigurationManager cfgManager(true); cfgManager.readConfiguration(variables, desc); - const auto essFile = variables["mwsave"].as(); - const auto outputFile = variables["output"].as(); + const auto& essFile = variables["mwsave"].as(); + const auto& outputFile = variables["output"].as(); std::string encoding = variables["encoding"].as(); ESSImport::Importer importer(essFile, outputFile, encoding); diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 87cee06e5d..3d9fc6edde 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -33,31 +33,21 @@ set(LAUNCHER_HEADER utils/openalutil.hpp ) -# Headers that must be pre-processed -set(LAUNCHER_UI - ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui - ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui - ${CMAKE_SOURCE_DIR}/files/ui/mainwindow.ui - ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui - ${CMAKE_SOURCE_DIR}/files/ui/importpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui - ${CMAKE_SOURCE_DIR}/files/ui/directorypicker.ui -) - source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) set(QT_USE_QTGUI 1) +set (LAUNCHER_RES ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) + # Set some platform specific settings if(WIN32) + set(LAUNCHER_RES ${LAUNCHER_RES} ${CMAKE_SOURCE_DIR}/files/windows/QWindowsVistaDark/dark.qrc) set(GUI_TYPE WIN32) set(QT_USE_QTMAIN TRUE) endif(WIN32) -QT_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) -QT_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) +QT_ADD_RESOURCES(RCC_SRCS ${LAUNCHER_RES}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) include_directories(${LIBUNSHIELD_INCLUDE_DIR}) endif(NOT WIN32) @@ -68,21 +58,21 @@ openmw_add_executable(openmw-launcher ${LAUNCHER} ${LAUNCHER_HEADER} ${RCC_SRCS} - ${MOC_SRCS} - ${UI_HDRS} ) +add_dependencies(openmw-launcher qm-files) + if (WIN32) INSTALL(TARGETS openmw-launcher RUNTIME DESTINATION ".") endif (WIN32) target_link_libraries(openmw-launcher - ${SDL2_LIBRARY_ONLY} + SDL2::SDL2 ${OPENAL_LIBRARY} components_qt ) -target_link_libraries(openmw-launcher Qt::Widgets Qt::Core) +target_link_libraries(openmw-launcher Qt::Widgets Qt::Core Qt::Svg) if (BUILD_WITH_CODE_COVERAGE) target_compile_options(openmw-launcher PRIVATE --coverage) @@ -91,9 +81,11 @@ endif() if(USE_QT) set_property(TARGET openmw-launcher PROPERTY AUTOMOC ON) + set_property(TARGET openmw-launcher PROPERTY AUTOUIC ON) + set_property(TARGET openmw-launcher PROPERTY AUTOUIC_SEARCH_PATHS ui) endif(USE_QT) -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-launcher PRIVATE diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 756aaba131..9bb54fdb8e 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -1,9 +1,13 @@ #include "datafilespage.hpp" #include "maindialog.hpp" +#include #include +#include #include +#include #include +#include #include #include @@ -26,8 +30,9 @@ #include #include #include -#include +#include #include +#include #include "utils/profilescombobox.hpp" #include "utils/textinputdialog.hpp" @@ -40,17 +45,61 @@ namespace { void contentSubdirs(const QString& path, QStringList& dirs) { - QStringList fileFilter{ "*.esm", "*.esp", "*.omwaddon", "*.bsa", "*.ba2", "*.omwscripts" }; - QStringList dirFilter{ "bookart", "icons", "meshes", "music", "sound", "textures" }; + static const QStringList fileFilter{ + "*.esm", + "*.esp", + "*.bsa", + "*.ba2", + "*.omwgame", + "*.omwaddon", + "*.omwscripts", + }; + + static const QStringList dirFilter{ + "animations", + "bookart", + "fonts", + "icons", + "interface", + "l10n", + "meshes", + "music", + "mygui", + "scripts", + "shaders", + "sound", + "splash", + "strings", + "textures", + "trees", + "video", + }; QDir currentDir(path); if (!currentDir.entryInfoList(fileFilter, QDir::Files).empty() || !currentDir.entryInfoList(dirFilter, QDir::Dirs | QDir::NoDotAndDotDot).empty()) + { dirs.push_back(currentDir.canonicalPath()); + return; + } for (const auto& subdir : currentDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) contentSubdirs(subdir.canonicalFilePath(), dirs); } + + QList> sortedSelectedItems(QListWidget* list, bool reverse = false) + { + QList> sortedItems; + for (QListWidgetItem* item : list->selectedItems()) + sortedItems.append(qMakePair(list->row(item), item)); + + if (reverse) + std::sort(sortedItems.begin(), sortedItems.end(), [](auto a, auto b) { return a.first > b.first; }); + else + std::sort(sortedItems.begin(), sortedItems.end(), [](auto a, auto b) { return a.first < b.first; }); + + return sortedItems; + } } namespace Launcher @@ -96,28 +145,7 @@ namespace Launcher int getMaxNavMeshDbFileSizeMiB() { - return Settings::Manager::getUInt64("max navmeshdb file size", "Navigator") / (1024 * 1024); - } - - std::optional findFirstPath(const QStringList& directories, const QString& fileName) - { - for (const QString& directoryPath : directories) - { - const QString filePath = QDir(directoryPath).absoluteFilePath(fileName); - if (QFile::exists(filePath)) - return filePath; - } - return std::nullopt; - } - - QStringList findAllFilePaths(const QStringList& directories, const QStringList& fileNames) - { - QStringList result; - result.reserve(fileNames.size()); - for (const QString& fileName : fileNames) - if (const auto filepath = findFirstPath(directories, fileName)) - result.append(*filepath); - return result; + return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024); } } } @@ -134,14 +162,17 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C ui.setupUi(this); setObjectName("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true); - const QString encoding = mGameSettings.value("encoding", "win1252"); + const QString encoding = mGameSettings.value("encoding", { "win1252" }).value; mSelector->setEncoding(encoding); - QStringList languages; - languages << tr("English") << tr("French") << tr("German") << tr("Italian") << tr("Polish") << tr("Russian") - << tr("Spanish"); + QVector> languages = { { "English", tr("English") }, { "French", tr("French") }, + { "German", tr("German") }, { "Italian", tr("Italian") }, { "Polish", tr("Polish") }, + { "Russian", tr("Russian") }, { "Spanish", tr("Spanish") } }; - mSelector->languageBox()->addItems(languages); + for (auto lang : languages) + { + mSelector->languageBox()->addItem(lang.second, lang.first); + } mNewProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); mCloneProfileDialog = new TextInputDialog(tr("Clone Content List"), tr("Content List name:"), this); @@ -150,13 +181,17 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C connect(mCloneProfileDialog->lineEdit(), &LineEdit::textChanged, this, &DataFilesPage::updateCloneProfileOkButton); connect(ui.directoryAddSubdirsButton, &QPushButton::released, this, [this]() { this->addSubdirectories(true); }); connect(ui.directoryInsertButton, &QPushButton::released, this, [this]() { this->addSubdirectories(false); }); - connect(ui.directoryUpButton, &QPushButton::released, this, [this]() { this->moveDirectory(-1); }); - connect(ui.directoryDownButton, &QPushButton::released, this, [this]() { this->moveDirectory(1); }); - connect(ui.directoryRemoveButton, &QPushButton::released, this, [this]() { this->removeDirectory(); }); - connect(ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveArchive(-1); }); - connect(ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveArchive(1); }); + connect(ui.directoryUpButton, &QPushButton::released, this, + [this]() { this->moveSources(ui.directoryListWidget, -1); }); + connect(ui.directoryDownButton, &QPushButton::released, this, + [this]() { this->moveSources(ui.directoryListWidget, 1); }); + connect(ui.directoryRemoveButton, &QPushButton::released, this, &DataFilesPage::removeDirectory); connect( - ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [this]() { this->sortDirectories(); }); + ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveSources(ui.archiveListWidget, -1); }); + connect( + ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveSources(ui.archiveListWidget, 1); }); + connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortDirectories); + connect(ui.archiveListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortArchives); buildView(); loadSettings(); @@ -209,6 +244,55 @@ void Launcher::DataFilesPage::buildView() &DataFilesPage::readNavMeshToolStderr); connect(mNavMeshToolInvoker->getProcess(), qOverload(&QProcess::finished), this, &DataFilesPage::navMeshToolFinished); + + buildArchiveContextMenu(); + buildDataFilesContextMenu(); +} + +void Launcher::DataFilesPage::slotCopySelectedItemsPaths() +{ + QClipboard* clipboard = QApplication::clipboard(); + QStringList filepaths; + + for (QListWidgetItem* item : ui.directoryListWidget->selectedItems()) + { + QString path = qvariant_cast(item->data(Qt::UserRole)).originalRepresentation; + filepaths.push_back(path); + } + + if (!filepaths.isEmpty()) + { + clipboard->setText(filepaths.join("\n")); + } +} + +void Launcher::DataFilesPage::slotOpenSelectedItemsPaths() +{ + QListWidgetItem* item = ui.directoryListWidget->currentItem(); + QUrl confFolderUrl = QUrl::fromLocalFile(qvariant_cast(item->data(Qt::UserRole)).value); + QDesktopServices::openUrl(confFolderUrl); +} + +void Launcher::DataFilesPage::buildArchiveContextMenu() +{ + connect(ui.archiveListWidget, &QListWidget::customContextMenuRequested, this, + &DataFilesPage::slotShowArchiveContextMenu); + + mArchiveContextMenu = new QMenu(ui.archiveListWidget); + mArchiveContextMenu->addAction(tr("&Check Selected"), this, SLOT(slotCheckMultiSelectedItems())); + mArchiveContextMenu->addAction(tr("&Uncheck Selected"), this, SLOT(slotUncheckMultiSelectedItems())); +} + +void Launcher::DataFilesPage::buildDataFilesContextMenu() +{ + connect(ui.directoryListWidget, &QListWidget::customContextMenuRequested, this, + &DataFilesPage::slotShowDataFilesContextMenu); + + mDataFilesContextMenu = new QMenu(ui.directoryListWidget); + mDataFilesContextMenu->addAction( + tr("&Copy Path(s) to Clipboard"), this, &Launcher::DataFilesPage::slotCopySelectedItemsPaths); + mDataFilesContextMenu->addAction( + tr("&Open Path in File Explorer"), this, &Launcher::DataFilesPage::slotOpenSelectedItemsPaths); } bool Launcher::DataFilesPage::loadSettings() @@ -227,9 +311,17 @@ bool Launcher::DataFilesPage::loadSettings() if (!currentProfile.isEmpty()) addProfile(currentProfile, true); - const int index = mSelector->languageBox()->findText(mLauncherSettings.getLanguage()); - if (index != -1) - mSelector->languageBox()->setCurrentIndex(index); + auto language = mLauncherSettings.getLanguage(); + + for (int i = 0; i < mSelector->languageBox()->count(); ++i) + { + QString languageItem = mSelector->languageBox()->itemData(i).toString(); + if (language == languageItem) + { + mSelector->languageBox()->setCurrentIndex(i); + break; + } + } return true; } @@ -240,101 +332,144 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) ui.archiveListWidget->clear(); ui.directoryListWidget->clear(); - QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName); - if (directories.isEmpty()) - directories = mGameSettings.getDataDirs(); + QList directories = mGameSettings.getDataDirs(); + QStringList contentModelDirectories = mLauncherSettings.getDataDirectoryList(contentModelName); + if (!contentModelDirectories.isEmpty()) + { + directories.erase(std::remove_if(directories.begin(), directories.end(), + [&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }), + directories.end()); + for (const auto& dir : contentModelDirectories) + directories.push_back(mGameSettings.processPathSettingValue({ dir })); + } mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) - directories.insert(0, mDataLocal); + directories.insert(0, { mDataLocal }); - const auto& globalDataDir = mGameSettings.getGlobalDataDir(); - if (!globalDataDir.empty()) - directories.insert(0, Files::pathToQString(globalDataDir)); + const auto& resourcesVfs = mGameSettings.getResourcesVfs(); + if (!resourcesVfs.isEmpty()) + directories.insert(0, { resourcesVfs }); std::unordered_set visitedDirectories; - for (const QString& currentDir : directories) + for (const Config::SettingValue& currentDir : directories) { - // normalize user supplied directories: resolve symlink, convert to native separator, make absolute - const QString canonicalDirPath = QDir(QDir::cleanPath(currentDir)).canonicalPath(); - - if (!visitedDirectories.insert(canonicalDirPath).second) + if (!visitedDirectories.insert(currentDir.value).second) continue; // add new achives files presents in current directory - addArchivesFromDir(currentDir); + addArchivesFromDir(currentDir.value); - QString tooltip; + QStringList tooltip; // add content files presents in current directory - mSelector->addFiles(currentDir, mNewDataDirs.contains(canonicalDirPath)); + mSelector->addFiles(currentDir.value, mNewDataDirs.contains(currentDir.value)); // add current directory to list - ui.directoryListWidget->addItem(currentDir); + ui.directoryListWidget->addItem(currentDir.originalRepresentation); auto row = ui.directoryListWidget->count() - 1; auto* item = ui.directoryListWidget->item(row); + item->setData(Qt::UserRole, QVariant::fromValue(currentDir)); - // Display new content with green background - if (mNewDataDirs.contains(canonicalDirPath)) + if (currentDir.value != currentDir.originalRepresentation) + tooltip << tr("Resolved as %1").arg(currentDir.value); + + // Display new content with custom formatting + if (mNewDataDirs.contains(currentDir.value)) { - tooltip += "Will be added to the current profile\n"; - item->setBackground(Qt::green); - item->setForeground(Qt::black); + tooltip << tr("Will be added to the current profile"); + QFont font = item->font(); + font.setBold(true); + font.setItalic(true); + item->setFont(font); } - // deactivate data-local and global data directory: they are always included - if (currentDir == mDataLocal || Files::pathFromQString(currentDir) == globalDataDir) + // deactivate data-local and resources/vfs: they are always included + // same for ones from non-user config files + if (currentDir.value == mDataLocal || currentDir.value == resourcesVfs + || !mGameSettings.isUserSetting(currentDir)) { auto flags = item->flags(); item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); + if (currentDir.value == mDataLocal) + tooltip << tr("This is the data-local directory and cannot be disabled"); + else if (currentDir.value == resourcesVfs) + tooltip << tr("This directory is part of OpenMW and cannot be disabled"); + else + tooltip << tr("This directory is enabled in an openmw.cfg other than the user one"); } // Add a "data file" icon if the directory contains a content file - if (mSelector->containsDataFiles(currentDir)) + if (mSelector->containsDataFiles(currentDir.value)) { item->setIcon(QIcon(":/images/openmw-plugin.png")); - tooltip += "Contains content file(s)"; + + tooltip << tr("Contains content file(s)"); } else { // Pad to correct vertical alignment QPixmap pixmap(QSize(200, 200)); // Arbitrary big number, will be scaled down to widget size - pixmap.fill(ui.directoryListWidget->palette().base().color()); + pixmap.fill(QColor(0, 0, 0, 0)); auto emptyIcon = QIcon(pixmap); item->setIcon(emptyIcon); } - item->setToolTip(tooltip); + item->setToolTip(tooltip.join('\n')); } mSelector->sortFiles(); - QStringList selectedArchives = mLauncherSettings.getArchiveList(contentModelName); - if (selectedArchives.isEmpty()) - selectedArchives = mGameSettings.getArchiveList(); + QList selectedArchives = mGameSettings.getArchiveList(); + QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName); + if (!contentModelSelectedArchives.isEmpty()) + { + selectedArchives.erase(std::remove_if(selectedArchives.begin(), selectedArchives.end(), + [&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }), + selectedArchives.end()); + for (const auto& dir : contentModelSelectedArchives) + selectedArchives.push_back({ dir }); + } // sort and tick BSA according to profile int row = 0; for (const auto& archive : selectedArchives) { - const auto match = ui.archiveListWidget->findItems(archive, Qt::MatchExactly); + const auto match = ui.archiveListWidget->findItems(archive.value, Qt::MatchFixedString); if (match.isEmpty()) continue; const auto name = match[0]->text(); const auto oldrow = ui.archiveListWidget->row(match[0]); + // entries may be duplicated, e.g. if a content list predated a BSA being added to a non-user config file + if (oldrow < row) + continue; ui.archiveListWidget->takeItem(oldrow); ui.archiveListWidget->insertItem(row, name); ui.archiveListWidget->item(row)->setCheckState(Qt::Checked); + ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(archive)); + if (!mGameSettings.isUserSetting(archive)) + { + auto flags = ui.archiveListWidget->item(row)->flags(); + ui.archiveListWidget->item(row)->setFlags( + flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); + ui.archiveListWidget->item(row)->setToolTip( + tr("This archive is enabled in an openmw.cfg other than the user one")); + } row++; } - mSelector->setProfileContent( - findAllFilePaths(directories, mLauncherSettings.getContentListFiles(contentModelName))); + QStringList nonUserContent; + for (const auto& content : mGameSettings.getContentList()) + { + if (!mGameSettings.isUserSetting(content)) + nonUserContent.push_back(content.value); + } + mSelector->setNonUserContent(nonUserContent); + mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName)); } void Launcher::DataFilesPage::saveSettings(const QString& profile) { - if (const int value = ui.navMeshMaxSizeSpinBox->value(); value != getMaxNavMeshDbFileSizeMiB()) - Settings::Manager::setUInt64( - "max navmeshdb file size", "Navigator", static_cast(std::max(0, value)) * 1024 * 1024); + Settings::navigator().mMaxNavmeshdbFileSize.set( + static_cast(std::max(0, ui.navMeshMaxSizeSpinBox->value())) * 1024 * 1024); QString profileName = profile; @@ -355,47 +490,59 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile) { fileNames.append(item->fileName()); } - mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames); + QStringList dirNames; + for (const auto& dir : dirList) + { + if (mGameSettings.isUserSetting(dir)) + dirNames.push_back(dir.originalRepresentation); + } + QStringList archiveNames; + for (const auto& archive : selectedArchivePaths()) + { + if (mGameSettings.isUserSetting(archive)) + archiveNames.push_back(archive.originalRepresentation); + } + mLauncherSettings.setContentList(profileName, dirNames, archiveNames, fileNames); mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); - QString language(mSelector->languageBox()->currentText()); + QString language(mSelector->languageBox()->currentData().toString()); mLauncherSettings.setLanguage(language); if (language == QLatin1String("Polish")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1250" }); } else if (language == QLatin1String("Russian")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1251" }); } else { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1252" }); } } -QStringList Launcher::DataFilesPage::selectedDirectoriesPaths() const +QList Launcher::DataFilesPage::selectedDirectoriesPaths() const { - QStringList dirList; + QList dirList; for (int i = 0; i < ui.directoryListWidget->count(); ++i) { const QListWidgetItem* item = ui.directoryListWidget->item(i); if (item->flags() & Qt::ItemIsEnabled) - dirList.append(item->text()); + dirList.append(qvariant_cast(item->data(Qt::UserRole))); } return dirList; } -QStringList Launcher::DataFilesPage::selectedArchivePaths() const +QList Launcher::DataFilesPage::selectedArchivePaths() const { - QStringList archiveList; + QList archiveList; for (int i = 0; i < ui.archiveListWidget->count(); ++i) { const QListWidgetItem* item = ui.archiveListWidget->item(i); if (item->checkState() == Qt::Checked) - archiveList.append(item->text()); + archiveList.append(qvariant_cast(item->data(Qt::UserRole))); } return archiveList; } @@ -549,7 +696,20 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered() if (profile.isEmpty()) return; - mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), selectedFilePaths()); + const auto& dirList = selectedDirectoriesPaths(); + QStringList dirNames; + for (const auto& dir : dirList) + { + if (mGameSettings.isUserSetting(dir)) + dirNames.push_back(dir.originalRepresentation); + } + QStringList archiveNames; + for (const auto& archive : selectedArchivePaths()) + { + if (mGameSettings.isUserSetting(archive)) + archiveNames.push_back(archive.originalRepresentation); + } + mLauncherSettings.setContentList(profile, dirNames, archiveNames, selectedFilePaths()); addProfile(profile, true); } @@ -587,41 +747,49 @@ void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString& text) mCloneProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1); } -QString Launcher::DataFilesPage::selectDirectory() -{ - QFileDialog fileDialog(this); - fileDialog.setFileMode(QFileDialog::Directory); - fileDialog.setOptions(QFileDialog::Option::ShowDirsOnly | QFileDialog::Option::ReadOnly); - - if (fileDialog.exec() == QDialog::Rejected) - return {}; - - return QDir(fileDialog.selectedFiles()[0]).canonicalPath(); -} - void Launcher::DataFilesPage::addSubdirectories(bool append) { - int selectedRow = append ? ui.directoryListWidget->count() : ui.directoryListWidget->currentRow(); + int selectedRow = -1; + if (append) + { + selectedRow = ui.directoryListWidget->count(); + } + else + { + const QList> sortedItems = sortedSelectedItems(ui.directoryListWidget); + if (!sortedItems.isEmpty()) + selectedRow = sortedItems.first().first; + } if (selectedRow == -1) return; - const auto rootDir = selectDirectory(); - if (rootDir.isEmpty()) + QString rootPath = QFileDialog::getExistingDirectory( + this, tr("Select Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly); + + if (rootPath.isEmpty()) return; - QStringList subdirs; - contentSubdirs(rootDir, subdirs); + const QDir rootDir(rootPath); + rootPath = rootDir.canonicalPath(); - if (subdirs.empty()) + QStringList subdirs; + contentSubdirs(rootPath, subdirs); + + // Always offer to append the root directory just in case + if (subdirs.isEmpty() || subdirs[0] != rootPath) + subdirs.prepend(rootPath); + else if (subdirs.size() == 1) { - // we didn't find anything that looks like a content directory, add directory selected by user - if (ui.directoryListWidget->findItems(rootDir, Qt::MatchFixedString).isEmpty()) - { - ui.directoryListWidget->addItem(rootDir); - mNewDataDirs.push_back(rootDir); - refreshDataFilesView(); - } + // We didn't find anything else that looks like a content directory + // Automatically add the directory selected by user + if (!ui.directoryListWidget->findItems(rootPath, Qt::MatchFixedString).isEmpty()) + return; + ui.directoryListWidget->insertItem(selectedRow, rootPath); + auto* item = ui.directoryListWidget->item(selectedRow); + item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ rootPath })); + mNewDataDirs.push_back(rootPath); + refreshDataFilesView(); return; } @@ -649,8 +817,11 @@ void Launcher::DataFilesPage::addSubdirectories(bool append) const auto* dir = select.dirListWidget->item(i); if (dir->checkState() == Qt::Checked) { - ui.directoryListWidget->insertItem(selectedRow++, dir->text()); + ui.directoryListWidget->insertItem(selectedRow, dir->text()); + auto* item = ui.directoryListWidget->item(selectedRow); + item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ dir->text() })); mNewDataDirs.push_back(dir->text()); + ++selectedRow; } } @@ -672,19 +843,19 @@ void Launcher::DataFilesPage::sortDirectories() } } -void Launcher::DataFilesPage::moveDirectory(int step) +void Launcher::DataFilesPage::sortArchives() { - int selectedRow = ui.directoryListWidget->currentRow(); - int newRow = selectedRow + step; - if (selectedRow == -1 || newRow < 0 || newRow > ui.directoryListWidget->count() - 1) - return; - - if (!(ui.directoryListWidget->item(newRow)->flags() & Qt::ItemIsEnabled)) - return; - - const auto item = ui.directoryListWidget->takeItem(selectedRow); - ui.directoryListWidget->insertItem(newRow, item); - ui.directoryListWidget->setCurrentRow(newRow); + // Ensure disabled entries (aka ones from non-user config files) are always at the top. + for (auto i = 1; i < ui.archiveListWidget->count(); ++i) + { + if (!(ui.archiveListWidget->item(i)->flags() & Qt::ItemIsEnabled) + && (ui.archiveListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled)) + { + const auto item = ui.archiveListWidget->takeItem(i); + ui.archiveListWidget->insertItem(i - 1, item); + ui.archiveListWidget->setCurrentRow(i); + } + } } void Launcher::DataFilesPage::removeDirectory() @@ -694,17 +865,55 @@ void Launcher::DataFilesPage::removeDirectory() refreshDataFilesView(); } -void Launcher::DataFilesPage::moveArchive(int step) +void Launcher::DataFilesPage::slotShowArchiveContextMenu(const QPoint& pos) { - int selectedRow = ui.archiveListWidget->currentRow(); - int newRow = selectedRow + step; - if (selectedRow == -1 || newRow < 0 || newRow > ui.archiveListWidget->count() - 1) - return; + QPoint globalPos = ui.archiveListWidget->viewport()->mapToGlobal(pos); + mArchiveContextMenu->exec(globalPos); +} - const auto* item = ui.archiveListWidget->takeItem(selectedRow); +void Launcher::DataFilesPage::slotShowDataFilesContextMenu(const QPoint& pos) +{ + QPoint globalPos = ui.directoryListWidget->viewport()->mapToGlobal(pos); + mDataFilesContextMenu->exec(globalPos); +} - addArchive(item->text(), item->checkState(), newRow); - ui.archiveListWidget->setCurrentRow(newRow); +void Launcher::DataFilesPage::setCheckStateForMultiSelectedItems(bool checked) +{ + Qt::CheckState checkState = checked ? Qt::Checked : Qt::Unchecked; + + for (QListWidgetItem* selectedItem : ui.archiveListWidget->selectedItems()) + { + selectedItem->setCheckState(checkState); + } +} + +void Launcher::DataFilesPage::slotUncheckMultiSelectedItems() +{ + setCheckStateForMultiSelectedItems(false); +} + +void Launcher::DataFilesPage::slotCheckMultiSelectedItems() +{ + setCheckStateForMultiSelectedItems(true); +} + +void Launcher::DataFilesPage::moveSources(QListWidget* sourceList, int step) +{ + const QList> sortedItems = sortedSelectedItems(sourceList, step > 0); + for (const auto& i : sortedItems) + { + int selectedRow = sourceList->row(i.second); + int newRow = selectedRow + step; + if (selectedRow == -1 || newRow < 0 || newRow > sourceList->count() - 1) + break; + + if (!(sourceList->item(newRow)->flags() & Qt::ItemIsEnabled)) + break; + + const auto item = sourceList->takeItem(selectedRow); + sourceList->insertItem(newRow, item); + sourceList->setCurrentRow(newRow); + } } void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState selected, int row) @@ -713,10 +922,14 @@ void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState sel row = ui.archiveListWidget->count(); ui.archiveListWidget->insertItem(row, name); ui.archiveListWidget->item(row)->setCheckState(selected); + ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ name })); if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ??? { - ui.archiveListWidget->item(row)->setBackground(Qt::green); - ui.archiveListWidget->item(row)->setForeground(Qt::black); + auto item = ui.archiveListWidget->item(row); + QFont font = item->font(); + font.setBold(true); + font.setItalic(true); + item->setFont(font); } } @@ -725,19 +938,19 @@ void Launcher::DataFilesPage::addArchivesFromDir(const QString& path) QStringList archiveFilter{ "*.bsa", "*.ba2" }; QDir dir(path); - std::unordered_set archives; + std::unordered_set archives; for (int i = 0; i < ui.archiveListWidget->count(); ++i) - archives.insert(ui.archiveListWidget->item(i)->text()); + archives.insert(VFS::Path::normalizedFromQString(ui.archiveListWidget->item(i)->text())); for (const auto& fileinfo : dir.entryInfoList(archiveFilter)) { const auto absPath = fileinfo.absoluteFilePath(); - if (Bsa::BSAFile::detectVersion(Files::pathFromQString(absPath)) == Bsa::BSAVER_UNKNOWN) + if (Bsa::BSAFile::detectVersion(Files::pathFromQString(absPath)) == Bsa::BsaVersion::Unknown) continue; const auto fileName = fileinfo.fileName(); - if (archives.insert(fileName).second) + if (archives.insert(VFS::Path::normalizedFromQString(fileName)).second) addArchive(fileName, Qt::Unchecked); } } diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 033c91f9c7..5d03cdf800 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -24,6 +25,7 @@ namespace ContentSelectorView namespace Config { class GameSettings; + struct SettingValue; class LauncherSettings; } @@ -39,6 +41,8 @@ namespace Launcher ContentSelectorView::ContentSelector* mSelector; Ui::DataFilesPage ui; + QMenu* mArchiveContextMenu; + QMenu* mDataFilesContextMenu; public: explicit DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, @@ -71,9 +75,14 @@ namespace Launcher void updateCloneProfileOkButton(const QString& text); void addSubdirectories(bool append); void sortDirectories(); + void sortArchives(); void removeDirectory(); - void moveArchive(int step); - void moveDirectory(int step); + void moveSources(QListWidget* sourceList, int step); + + void slotShowArchiveContextMenu(const QPoint& pos); + void slotShowDataFilesContextMenu(const QPoint& pos); + void slotCheckMultiSelectedItems(); + void slotUncheckMultiSelectedItems(); void on_newProfileAction_triggered(); void on_cloneProfileAction_triggered(); @@ -121,6 +130,9 @@ namespace Launcher void addArchive(const QString& name, Qt::CheckState selected, int row = -1); void addArchivesFromDir(const QString& dir); void buildView(); + void buildArchiveContextMenu(); + void buildDataFilesContextMenu(); + void setCheckStateForMultiSelectedItems(bool checked); void setProfile(int index, bool savePrevious); void setProfile(const QString& previous, const QString& current, bool savePrevious); void removeProfile(const QString& profile); @@ -131,15 +143,16 @@ namespace Launcher void reloadCells(QStringList selectedFiles); void refreshDataFilesView(); void updateNavMeshProgress(int minDataSize); - QString selectDirectory(); + void slotCopySelectedItemsPaths(); + void slotOpenSelectedItemsPaths(); /** * Returns the file paths of all selected content files * @return the file paths of all selected content files */ QStringList selectedFilePaths() const; - QStringList selectedArchivePaths() const; - QStringList selectedDirectoriesPaths() const; + QList selectedArchivePaths() const; + QList selectedDirectoriesPaths() const; }; } #endif diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 5f37c0defe..735bcf1df1 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -2,6 +2,9 @@ #include "sdlinit.hpp" +#include +#include + #include #include @@ -14,22 +17,6 @@ #include #include -#include - -QString getAspect(int x, int y) -{ - int gcd = std::gcd(x, y); - if (gcd == 0) - return QString(); - - int xaspect = x / gcd; - int yaspect = y / gcd; - // special case: 8 : 5 is usually referred to as 16:10 - if (xaspect == 8 && yaspect == 5) - return QString("16:10"); - - return QString(QString::number(xaspect) + ":" + QString::number(yaspect)); -} Launcher::GraphicsPage::GraphicsPage(QWidget* parent) : QWidget(parent) @@ -47,7 +34,6 @@ Launcher::GraphicsPage::GraphicsPage(QWidget* parent) connect(standardRadioButton, &QRadioButton::toggled, this, &GraphicsPage::slotStandardToggled); connect(screenComboBox, qOverload(&QComboBox::currentIndexChanged), this, &GraphicsPage::screenChanged); connect(framerateLimitCheckBox, &QCheckBox::toggled, this, &GraphicsPage::slotFramerateLimitToggled); - connect(shadowDistanceCheckBox, &QCheckBox::toggled, this, &GraphicsPage::slotShadowDistLimitToggled); } bool Launcher::GraphicsPage::setupSDL() @@ -94,32 +80,29 @@ bool Launcher::GraphicsPage::loadSettings() // Visuals - int vsync = Settings::Manager::getInt("vsync mode", "Video"); - if (vsync < 0 || vsync > 2) - vsync = 0; + const int vsync = Settings::video().mVsyncMode; vSyncComboBox->setCurrentIndex(vsync); - size_t windowMode = static_cast(Settings::Manager::getInt("window mode", "Video")); - if (windowMode > static_cast(Settings::WindowMode::Windowed)) - windowMode = 0; - windowModeComboBox->setCurrentIndex(windowMode); - slotFullScreenChanged(windowMode); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; - if (Settings::Manager::getBool("window border", "Video")) + windowModeComboBox->setCurrentIndex(static_cast(windowMode)); + handleWindowModeChange(windowMode); + + if (Settings::video().mWindowBorder) windowBorderCheckBox->setCheckState(Qt::Checked); // aaValue is the actual value (0, 1, 2, 4, 8, 16) - int aaValue = Settings::Manager::getInt("antialiasing", "Video"); + const int aaValue = Settings::video().mAntialiasing; // aaIndex is the index into the allowed values in the pull down. - int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); + const int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); - int width = Settings::Manager::getInt("resolution x", "Video"); - int height = Settings::Manager::getInt("resolution y", "Video"); - QString resolution = QString::number(width) + QString(" x ") + QString::number(height); - screenComboBox->setCurrentIndex(Settings::Manager::getInt("screen", "Video")); + const int width = Settings::video().mResolutionX; + const int height = Settings::video().mResolutionY; + QString resolution = QString::number(width) + QString(" × ") + QString::number(height); + screenComboBox->setCurrentIndex(Settings::video().mScreen); int resIndex = resolutionComboBox->findText(resolution, Qt::MatchStartsWith); @@ -135,52 +118,13 @@ bool Launcher::GraphicsPage::loadSettings() customHeightSpinBox->setValue(height); } - float fpsLimit = Settings::Manager::getFloat("framerate limit", "Video"); + const float fpsLimit = Settings::video().mFramerateLimit; if (fpsLimit != 0) { framerateLimitCheckBox->setCheckState(Qt::Checked); framerateLimitSpinBox->setValue(fpsLimit); } - // Lighting - int lightingMethod = 1; - if (Settings::Manager::getString("lighting method", "Shaders") == "legacy") - lightingMethod = 0; - else if (Settings::Manager::getString("lighting method", "Shaders") == "shaders") - lightingMethod = 2; - lightingMethodComboBox->setCurrentIndex(lightingMethod); - - // Shadows - if (Settings::Manager::getBool("actor shadows", "Shadows")) - actorShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("player shadows", "Shadows")) - playerShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - terrainShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("object shadows", "Shadows")) - objectShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) - indoorShadowsCheckBox->setCheckState(Qt::Checked); - - shadowComputeSceneBoundsComboBox->setCurrentIndex(shadowComputeSceneBoundsComboBox->findText( - QString(tr(Settings::Manager::getString("compute scene bounds", "Shadows").c_str())))); - - int shadowDistLimit = Settings::Manager::getInt("maximum shadow map distance", "Shadows"); - if (shadowDistLimit > 0) - { - shadowDistanceCheckBox->setCheckState(Qt::Checked); - shadowDistanceSpinBox->setValue(shadowDistLimit); - } - - float shadowFadeStart = Settings::Manager::getFloat("shadow fade start", "Shadows"); - if (shadowFadeStart != 0) - fadeStartSpinBox->setValue(shadowFadeStart); - - int shadowRes = Settings::Manager::getInt("shadow map resolution", "Shadows"); - int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); - if (shadowResIndex != -1) - shadowResolutionComboBox->setCurrentIndex(shadowResIndex); - return true; } @@ -188,29 +132,16 @@ void Launcher::GraphicsPage::saveSettings() { // Visuals - // Ensure we only set the new settings if they changed. This is to avoid cluttering the - // user settings file (which by definition should only contain settings the user has touched) - int cVSync = vSyncComboBox->currentIndex(); - if (cVSync != Settings::Manager::getInt("vsync mode", "Video")) - Settings::Manager::setInt("vsync mode", "Video", cVSync); - - int cWindowMode = windowModeComboBox->currentIndex(); - if (cWindowMode != Settings::Manager::getInt("window mode", "Video")) - Settings::Manager::setInt("window mode", "Video", cWindowMode); - - bool cWindowBorder = windowBorderCheckBox->checkState(); - if (cWindowBorder != Settings::Manager::getBool("window border", "Video")) - Settings::Manager::setBool("window border", "Video", cWindowBorder); - - int cAAValue = antiAliasingComboBox->currentText().toInt(); - if (cAAValue != Settings::Manager::getInt("antialiasing", "Video")) - Settings::Manager::setInt("antialiasing", "Video", cAAValue); + Settings::video().mVsyncMode.set(static_cast(vSyncComboBox->currentIndex())); + Settings::video().mWindowMode.set(static_cast(windowModeComboBox->currentIndex())); + Settings::video().mWindowBorder.set(windowBorderCheckBox->checkState() == Qt::Checked); + Settings::video().mAntialiasing.set(antiAliasingComboBox->currentText().toInt()); int cWidth = 0; int cHeight = 0; if (standardRadioButton->isChecked()) { - QRegularExpression resolutionRe("^(\\d+) x (\\d+)"); + QRegularExpression resolutionRe("^(\\d+) × (\\d+)"); QRegularExpressionMatch match = resolutionRe.match(resolutionComboBox->currentText().simplified()); if (match.hasMatch()) { @@ -224,83 +155,18 @@ void Launcher::GraphicsPage::saveSettings() cHeight = customHeightSpinBox->value(); } - if (cWidth != Settings::Manager::getInt("resolution x", "Video")) - Settings::Manager::setInt("resolution x", "Video", cWidth); - - if (cHeight != Settings::Manager::getInt("resolution y", "Video")) - Settings::Manager::setInt("resolution y", "Video", cHeight); - - int cScreen = screenComboBox->currentIndex(); - if (cScreen != Settings::Manager::getInt("screen", "Video")) - Settings::Manager::setInt("screen", "Video", cScreen); + Settings::video().mResolutionX.set(cWidth); + Settings::video().mResolutionY.set(cHeight); + Settings::video().mScreen.set(screenComboBox->currentIndex()); if (framerateLimitCheckBox->checkState() != Qt::Unchecked) { - float cFpsLimit = framerateLimitSpinBox->value(); - if (cFpsLimit != Settings::Manager::getFloat("framerate limit", "Video")) - Settings::Manager::setFloat("framerate limit", "Video", cFpsLimit); + Settings::video().mFramerateLimit.set(framerateLimitSpinBox->value()); } - else if (Settings::Manager::getFloat("framerate limit", "Video") != 0) + else if (Settings::video().mFramerateLimit != 0) { - Settings::Manager::setFloat("framerate limit", "Video", 0); + Settings::video().mFramerateLimit.set(0); } - - // Lighting - static std::array lightingMethodMap = { "legacy", "shaders compatibility", "shaders" }; - const std::string& cLightingMethod = lightingMethodMap[lightingMethodComboBox->currentIndex()]; - if (cLightingMethod != Settings::Manager::getString("lighting method", "Shaders")) - Settings::Manager::setString("lighting method", "Shaders", cLightingMethod); - - // Shadows - int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; - if (Settings::Manager::getInt("maximum shadow map distance", "Shadows") != cShadowDist) - Settings::Manager::setInt("maximum shadow map distance", "Shadows", cShadowDist); - float cFadeStart = fadeStartSpinBox->value(); - if (cShadowDist > 0 && Settings::Manager::getFloat("shadow fade start", "Shadows") != cFadeStart) - Settings::Manager::setFloat("shadow fade start", "Shadows", cFadeStart); - - bool cActorShadows = actorShadowsCheckBox->checkState(); - bool cObjectShadows = objectShadowsCheckBox->checkState(); - bool cTerrainShadows = terrainShadowsCheckBox->checkState(); - bool cPlayerShadows = playerShadowsCheckBox->checkState(); - if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) - { - if (!Settings::Manager::getBool("enable shadows", "Shadows")) - Settings::Manager::setBool("enable shadows", "Shadows", true); - if (Settings::Manager::getBool("actor shadows", "Shadows") != cActorShadows) - Settings::Manager::setBool("actor shadows", "Shadows", cActorShadows); - if (Settings::Manager::getBool("player shadows", "Shadows") != cPlayerShadows) - Settings::Manager::setBool("player shadows", "Shadows", cPlayerShadows); - if (Settings::Manager::getBool("object shadows", "Shadows") != cObjectShadows) - Settings::Manager::setBool("object shadows", "Shadows", cObjectShadows); - if (Settings::Manager::getBool("terrain shadows", "Shadows") != cTerrainShadows) - Settings::Manager::setBool("terrain shadows", "Shadows", cTerrainShadows); - } - else - { - if (Settings::Manager::getBool("enable shadows", "Shadows")) - Settings::Manager::setBool("enable shadows", "Shadows", false); - if (Settings::Manager::getBool("actor shadows", "Shadows")) - Settings::Manager::setBool("actor shadows", "Shadows", false); - if (Settings::Manager::getBool("player shadows", "Shadows")) - Settings::Manager::setBool("player shadows", "Shadows", false); - if (Settings::Manager::getBool("object shadows", "Shadows")) - Settings::Manager::setBool("object shadows", "Shadows", false); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - Settings::Manager::setBool("terrain shadows", "Shadows", false); - } - - bool cIndoorShadows = indoorShadowsCheckBox->checkState(); - if (Settings::Manager::getBool("enable indoor shadows", "Shadows") != cIndoorShadows) - Settings::Manager::setBool("enable indoor shadows", "Shadows", cIndoorShadows); - - int cShadowRes = shadowResolutionComboBox->currentText().toInt(); - if (cShadowRes != Settings::Manager::getInt("shadow map resolution", "Shadows")) - Settings::Manager::setInt("shadow map resolution", "Shadows", cShadowRes); - - auto cComputeSceneBounds = shadowComputeSceneBoundsComboBox->currentText().toStdString(); - if (cComputeSceneBounds != Settings::Manager::getString("compute scene bounds", "Shadows")) - Settings::Manager::setString("compute scene bounds", "Shadows", cComputeSceneBounds); } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) @@ -335,19 +201,8 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) return result; } - QString resolution = QString::number(mode.w) + QString(" x ") + QString::number(mode.h); - - QString aspect = getAspect(mode.w, mode.h); - if (aspect == QLatin1String("16:9") || aspect == QLatin1String("16:10")) - { - resolution.append(tr("\t(Wide ") + aspect + ")"); - } - else if (aspect == QLatin1String("4:3")) - { - resolution.append(tr("\t(Standard 4:3)")); - } - - result.append(resolution); + auto str = Misc::getResolutionText(mode.w, mode.h, "%i × %i (%i:%i)"); + result.append(QString(str.c_str())); } result.removeDuplicates(); @@ -380,8 +235,12 @@ void Launcher::GraphicsPage::screenChanged(int screen) void Launcher::GraphicsPage::slotFullScreenChanged(int mode) { - if (mode == static_cast(Settings::WindowMode::Fullscreen) - || mode == static_cast(Settings::WindowMode::WindowedFullscreen)) + handleWindowModeChange(static_cast(mode)); +} + +void Launcher::GraphicsPage::handleWindowModeChange(Settings::WindowMode mode) +{ + if (mode == Settings::WindowMode::Fullscreen || mode == Settings::WindowMode::WindowedFullscreen) { standardRadioButton->toggle(); customRadioButton->setEnabled(false); @@ -418,9 +277,3 @@ void Launcher::GraphicsPage::slotFramerateLimitToggled(bool checked) { framerateLimitSpinBox->setEnabled(checked); } - -void Launcher::GraphicsPage::slotShadowDistLimitToggled(bool checked) -{ - shadowDistanceSpinBox->setEnabled(checked); - fadeStartSpinBox->setEnabled(checked); -} diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index 92bdf35ac4..928ec9f1a2 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -3,7 +3,7 @@ #include "ui_graphicspage.h" -#include +#include namespace Files { @@ -31,7 +31,6 @@ namespace Launcher void slotFullScreenChanged(int state); void slotStandardToggled(bool checked); void slotFramerateLimitToggled(bool checked); - void slotShadowDistLimitToggled(bool checked); private: QVector mResolutionsPerScreen; @@ -40,6 +39,7 @@ namespace Launcher static QRect getMaximumResolution(); bool setupSDL(); + void handleWindowModeChange(Settings::WindowMode state); }; } #endif diff --git a/apps/launcher/importpage.cpp b/apps/launcher/importpage.cpp index fa91ad1654..3ad6e538da 100644 --- a/apps/launcher/importpage.cpp +++ b/apps/launcher/importpage.cpp @@ -37,9 +37,9 @@ Launcher::ImportPage::ImportPage(const Files::ConfigurationManager& cfg, Config: // Detect Morrowind configuration files QStringList iniPaths; - for (const QString& path : mGameSettings.getDataDirs()) + for (const auto& path : mGameSettings.getDataDirs()) { - QDir dir(path); + QDir dir(path.value); dir.setPath(dir.canonicalPath()); // Resolve symlinks if (dir.exists(QString("Morrowind.ini"))) @@ -104,9 +104,9 @@ void Launcher::ImportPage::on_importerButton_clicked() msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("

Could not open or create %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

") + tr("

Could not open or create %1 for writing

" + "

Please make sure you have the right permissions " + "and try again.

") .arg(file.fileName())); msgBox.exec(); return; @@ -125,7 +125,7 @@ void Launcher::ImportPage::on_importerButton_clicked() arguments.append(QString("--fonts")); arguments.append(QString("--encoding")); - arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); + arguments.append(mGameSettings.value(QString("encoding"), { "win1252" }).value); arguments.append(QString("--ini")); arguments.append(settingsComboBox->currentText()); arguments.append(QString("--cfg")); @@ -220,9 +220,15 @@ void Launcher::ImportPage::resetProgressBar() progressBar->reset(); } -void Launcher::ImportPage::saveSettings() {} +void Launcher::ImportPage::saveSettings() +{ + mLauncherSettings.setImportContentSetup(addonsCheckBox->isChecked()); + mLauncherSettings.setImportFontSetup(fontsCheckBox->isChecked()); +} bool Launcher::ImportPage::loadSettings() { + addonsCheckBox->setChecked(mLauncherSettings.getImportContentSetup()); + fontsCheckBox->setChecked(mLauncherSettings.getImportFontSetup()); return true; } diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 4aac90fb6e..2ea152305f 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -1,13 +1,15 @@ #include #include -#include #include #include #include #include +#include +#include +#include #include #ifdef MAC_OS_X_VERSION_MIN_REQUIRED @@ -28,23 +30,19 @@ int runLauncher(int argc, char* argv[]) configurationManager.addCommonOptions(description); configurationManager.readConfiguration(variables, description, true); - setupLogging(configurationManager.getLogPath(), "Launcher"); + Debug::setupLogging(configurationManager.getLogPath(), "Launcher"); try { - QApplication app(argc, argv); + Platform::Application app(argc, argv); - // Internationalization - QString locale = QLocale::system().name().section('_', 0, 0); + QString resourcesPath("."); + if (!variables["resources"].empty()) + { + resourcesPath = Files::pathToQString(variables["resources"].as().u8string()); + } - QTranslator appTranslator; - appTranslator.load(":/translations/" + locale + ".qm"); - app.installTranslator(&appTranslator); - - // Now we make sure the current dir is set to application path - QDir dir(QCoreApplication::applicationDirPath()); - - QDir::setCurrent(dir.absolutePath()); + l10n::installQtTranslations(app, "launcher", resourcesPath); Launcher::MainDialog mainWin(configurationManager); @@ -68,5 +66,5 @@ int runLauncher(int argc, char* argv[]) int main(int argc, char* argv[]) { - return wrapApplication(runLauncher, argc, argv, "Launcher"); + return Debug::wrapApplication(runLauncher, argc, argv, "Launcher"); } diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 0caf7a576a..07face085f 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,13 +1,5 @@ #include "maindialog.hpp" -#include -#include -#include -#include -#include -#include -#include - #include #include #include @@ -15,16 +7,26 @@ #include #include +#include +#include #include #include #include +#include #include +#include +#include #include "datafilespage.hpp" #include "graphicspage.hpp" #include "importpage.hpp" #include "settingspage.hpp" +namespace +{ + constexpr const char* toolBarStyle = "QToolBar { border: 0px; } QToolButton { min-width: 70px }"; +} + using namespace Process; void cfgError(const QString& title, const QString& msg) @@ -53,9 +55,11 @@ Launcher::MainDialog::MainDialog(const Files::ConfigurationManager& configuratio &MainDialog::wizardFinished); buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr(" Launch OpenMW ")); + buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Launch OpenMW")); buttonBox->button(QDialogButtonBox::Help)->setText(tr("Help")); + buttonBox->button(QDialogButtonBox::Ok)->setMinimumWidth(160); + // Order of buttons can be different on different setups, // so make sure that the Play button has a focus by default. buttonBox->button(QDialogButtonBox::Ok)->setFocus(); @@ -68,6 +72,15 @@ Launcher::MainDialog::MainDialog(const Files::ConfigurationManager& configuratio setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); createIcons(); + + QWidget* spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolBar->addWidget(spacer); + + QLabel* logo = new QLabel(this); + logo->setPixmap(QIcon(":/images/openmw-header.png").pixmap(QSize(294, 64))); + toolBar->addWidget(logo); + toolBar->setStyleSheet(toolBarStyle); } Launcher::MainDialog::~MainDialog() @@ -76,10 +89,22 @@ Launcher::MainDialog::~MainDialog() delete mWizardInvoker; } +bool Launcher::MainDialog::event(QEvent* event) +{ + // Apply style sheet again if style was changed + if (event->type() == QEvent::PaletteChange) + { + if (toolBar != nullptr) + toolBar->setStyleSheet(toolBarStyle); + } + + return QMainWindow::event(event); +} + void Launcher::MainDialog::createIcons() { if (!QIcon::hasThemeIcon("document-new")) - QIcon::setThemeName("tango"); + QIcon::setThemeName("fallback"); connect(dataAction, &QAction::triggered, this, &MainDialog::enableDataPage); connect(graphicsAction, &QAction::triggered, this, &MainDialog::enableGraphicsPage); @@ -121,13 +146,14 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() const auto& userConfigDir = mCfgMgr.getUserConfigPath(); if (!exists(userConfigDir)) { - if (!create_directories(userConfigDir)) + std::error_code ec; + if (!create_directories(userConfigDir, ec)) { - cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not create directory %0

\ - Please make sure you have the right permissions \ - and try again.
") - .arg(Files::pathToQString(canonical(userConfigDir)))); + cfgError(tr("Error creating OpenMW configuration directory: code %0").arg(ec.value()), + tr("
Could not create directory %0

" + "%1
") + .arg(Files::pathToQString(userConfigDir)) + .arg(QString(ec.message().c_str()))); return FirstRunDialogResultFailure; } } @@ -139,10 +165,10 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() msgBox.setIcon(QMessageBox::Question); msgBox.setStandardButtons(QMessageBox::NoButton); msgBox.setText( - tr("

Welcome to OpenMW!

\ -

It is recommended to run the Installation Wizard.

\ -

The Wizard will let you select an existing Morrowind installation, \ - or install Morrowind for OpenMW to use.

")); + tr("

Welcome to OpenMW!

" + "

It is recommended to run the Installation Wizard.

" + "

The Wizard will let you select an existing Morrowind installation, " + "or install Morrowind for OpenMW to use.

")); QAbstractButton* wizardButton = msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! @@ -174,14 +200,13 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() void Launcher::MainDialog::setVersionLabel() { // Add version information to bottom of the window - Version::Version v = Version::getOpenmwVersion(mGameSettings.value("resources").toUtf8().constData()); - - QString revision(QString::fromUtf8(v.mCommitHash.c_str())); - QString tag(QString::fromUtf8(v.mTagHash.c_str())); + QString revision(QString::fromUtf8(Version::getCommitHash().data(), Version::getCommitHash().size())); + QString tag(QString::fromUtf8(Version::getTagHash().data(), Version::getTagHash().size())); versionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - if (!v.mVersion.empty() && (revision.isEmpty() || revision == tag)) - versionLabel->setText(tr("OpenMW %1 release").arg(QString::fromUtf8(v.mVersion.c_str()))); + if (!Version::getVersion().empty() && (revision.isEmpty() || revision == tag)) + versionLabel->setText( + tr("OpenMW %1 release").arg(QString::fromUtf8(Version::getVersion().data(), Version::getVersion().size()))); else versionLabel->setText(tr("OpenMW development (%1)").arg(revision.left(10))); @@ -295,15 +320,15 @@ bool Launcher::MainDialog::setupLauncherSettings() if (!QFile::exists(path)) return true; - Log(Debug::Verbose) << "Loading config file: " << path.toUtf8().constData(); + Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData(); QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading:

%1

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open %0 for reading:

%1

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName()) .arg(file.errorString())); return false; @@ -323,7 +348,7 @@ bool Launcher::MainDialog::setupGameSettings() QFile file; - auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, bool), + auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, const QString&, bool), bool ignoreContent = false) -> std::optional { file.setFileName(path); if (file.exists()) @@ -331,16 +356,16 @@ bool Launcher::MainDialog::setupGameSettings() if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open %0 for reading

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName())); return {}; } QTextStream stream(&file); Misc::ensureUtf8Encoding(stream); - (mGameSettings.*reader)(stream, ignoreContent); + (mGameSettings.*reader)(stream, QFileInfo(path).dir().path(), ignoreContent); file.close(); return true; } @@ -352,29 +377,24 @@ bool Launcher::MainDialog::setupGameSettings() if (!loadFile(Files::getUserConfigPathQString(mCfgMgr), &Config::GameSettings::readUserFile)) return false; - // Now the rest - priority: user > local > global - if (auto result = loadFile(Files::getLocalConfigPathQString(mCfgMgr), &Config::GameSettings::readFile, true)) + for (const auto& path : Files::getActiveConfigPathsQString(mCfgMgr)) { - // Load global if local wasn't found - if (!*result && !loadFile(Files::getGlobalConfigPathQString(mCfgMgr), &Config::GameSettings::readFile, true)) + Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData(); + if (!loadFile(path, &Config::GameSettings::readFile)) return false; } - else - return false; - if (!loadFile(Files::getUserConfigPathQString(mCfgMgr), &Config::GameSettings::readFile)) - return false; return true; } bool Launcher::MainDialog::setupGameData() { - QStringList dataDirs; + bool foundData = false; // Check if the paths actually contain data files - for (const QString& path3 : mGameSettings.getDataDirs()) + for (const auto& path3 : mGameSettings.getDataDirs()) { - QDir dir(path3); + QDir dir(path3.value); QStringList filters; filters << "*.esp" << "*.esm" @@ -382,18 +402,21 @@ bool Launcher::MainDialog::setupGameData() << "*.omwaddon"; if (!dir.entryList(filters).isEmpty()) - dataDirs.append(path3); + { + foundData = true; + break; + } } - if (dataDirs.isEmpty()) + if (!foundData) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::NoButton); msgBox.setText( - tr("
Could not find the Data Files location

\ - The directory containing the data files was not found.")); + tr("
Could not find the Data Files location

" + "The directory containing the data files was not found.")); QAbstractButton* wizardButton = msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); QAbstractButton* skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); @@ -423,8 +446,8 @@ bool Launcher::MainDialog::setupGraphicsSettings() catch (std::exception& e) { cfgError(tr("Error reading OpenMW configuration files"), - tr("
The problem may be due to an incomplete installation of OpenMW.
\ - Reinstalling OpenMW may resolve the problem.
") + tr("
The problem may be due to an incomplete installation of OpenMW.
" + "Reinstalling OpenMW may resolve the problem.
") + e.what()); return false; } @@ -461,13 +484,14 @@ bool Launcher::MainDialog::writeSettings() if (!exists(userPath)) { - if (!create_directories(userPath)) + std::error_code ec; + if (!create_directories(userPath, ec)) { - cfgError(tr("Error creating OpenMW configuration directory"), - tr("
Could not create %0

\ - Please make sure you have the right permissions \ - and try again.
") - .arg(Files::pathToQString(userPath))); + cfgError(tr("Error creating OpenMW configuration directory: code %0").arg(ec.value()), + tr("
Could not create directory %0

" + "%1
") + .arg(Files::pathToQString(userPath)) + .arg(QString(ec.message().c_str()))); return false; } } @@ -483,9 +507,9 @@ bool Launcher::MainDialog::writeSettings() { // File cannot be opened or created cfgError(tr("Error writing OpenMW configuration file"), - tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open or create %0 for writing

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName())); return false; } @@ -514,9 +538,9 @@ bool Launcher::MainDialog::writeSettings() { // File cannot be opened or created cfgError(tr("Error writing Launcher configuration file"), - tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
") + tr("
Could not open or create %0 for writing

" + "Please make sure you have the right permissions " + "and try again.
") .arg(file.fileName())); return false; } @@ -566,8 +590,8 @@ void Launcher::MainDialog::play() msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText( - tr("
You do not have a game file selected.

\ - OpenMW will not start without a game file selected.
")); + tr("
You do not have a game file selected.

" + "OpenMW will not start without a game file selected.
")); msgBox.exec(); return; } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index cc68d52c77..5ceee966ba 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -60,6 +60,9 @@ namespace Launcher void play(); void help(); + protected: + bool event(QEvent* event) override; + private slots: void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 07570272dd..dfddc45bc5 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -55,6 +55,20 @@ namespace { value.set(spinBox.value()); } + + int toIndex(Settings::HrtfMode value) + { + switch (value) + { + case Settings::HrtfMode::Auto: + return 0; + case Settings::HrtfMode::Disable: + return 1; + case Settings::HrtfMode::Enable: + return 2; + } + return 0; + } } Launcher::SettingsPage::SettingsPage(Config::GameSettings& gameSettings, QWidget* parent) @@ -175,11 +189,15 @@ bool Launcher::SettingsPage::loadSettings() loadSettingBool(Settings::game().mWeaponSheathing, *weaponSheathingCheckBox); loadSettingBool(Settings::game().mShieldSheathing, *shieldSheathingCheckBox); } + loadSettingBool(Settings::game().mSmoothAnimTransitions, *smoothAnimTransitionsCheckBox); loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox); loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox); + loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox); - distantLandCheckBox->setCheckState( - Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging ? Qt::Checked : Qt::Unchecked); + connect(distantLandCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotDistantLandToggled); + bool distantLandEnabled = Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging; + distantLandCheckBox->setCheckState(distantLandEnabled ? Qt::Checked : Qt::Unchecked); + slotDistantLandToggled(distantLandEnabled); loadSettingBool(Settings::terrain().mObjectPagingActiveGrid, *activeGridObjectPagingCheckBox); viewingDistanceComboBox->setValue(convertToCells(Settings::camera().mViewingDistance)); @@ -197,6 +215,60 @@ bool Launcher::SettingsPage::loadSettings() loadSettingBool(Settings::fog().mExponentialFog, *exponentialFogCheckBox); loadSettingBool(Settings::fog().mSkyBlending, *skyBlendingCheckBox); skyBlendingStartComboBox->setValue(Settings::fog().mSkyBlendingStart); + + loadSettingBool(Settings::shadows().mActorShadows, *actorShadowsCheckBox); + loadSettingBool(Settings::shadows().mPlayerShadows, *playerShadowsCheckBox); + loadSettingBool(Settings::shadows().mTerrainShadows, *terrainShadowsCheckBox); + loadSettingBool(Settings::shadows().mObjectShadows, *objectShadowsCheckBox); + loadSettingBool(Settings::shadows().mEnableIndoorShadows, *indoorShadowsCheckBox); + + const auto& boundMethod = Settings::shadows().mComputeSceneBounds.get(); + if (boundMethod == "bounds") + shadowComputeSceneBoundsComboBox->setCurrentIndex(0); + else if (boundMethod == "primitives") + shadowComputeSceneBoundsComboBox->setCurrentIndex(1); + else + shadowComputeSceneBoundsComboBox->setCurrentIndex(2); + + const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; + if (shadowDistLimit > 0) + { + shadowDistanceCheckBox->setCheckState(Qt::Checked); + shadowDistanceSpinBox->setValue(shadowDistLimit); + shadowDistanceSpinBox->setEnabled(true); + fadeStartSpinBox->setEnabled(true); + } + + const float shadowFadeStart = Settings::shadows().mShadowFadeStart; + if (shadowFadeStart != 0) + fadeStartSpinBox->setValue(shadowFadeStart); + + const int shadowRes = Settings::shadows().mShadowMapResolution; + int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); + if (shadowResIndex != -1) + shadowResolutionComboBox->setCurrentIndex(shadowResIndex); + else + { + shadowResolutionComboBox->addItem(QString::number(shadowRes)); + shadowResolutionComboBox->setCurrentIndex(shadowResolutionComboBox->count() - 1); + } + + connect(shadowDistanceCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotShadowDistLimitToggled); + + int lightingMethod = 1; + switch (Settings::shaders().mLightingMethod) + { + case SceneUtil::LightingMethod::FFP: + lightingMethod = 0; + break; + case SceneUtil::LightingMethod::PerObjectUniform: + lightingMethod = 1; + break; + case SceneUtil::LightingMethod::SingleUBO: + lightingMethod = 2; + break; + } + lightingMethodComboBox->setCurrentIndex(lightingMethod); } // Audio @@ -210,11 +282,7 @@ bool Launcher::SettingsPage::loadSettings() audioDeviceSelectorComboBox->setCurrentIndex(audioDeviceIndex); } } - const int hrtfEnabledIndex = Settings::sound().mHrtfEnable; - if (hrtfEnabledIndex >= -1 && hrtfEnabledIndex <= 1) - { - enableHRTFComboBox->setCurrentIndex(hrtfEnabledIndex + 1); - } + enableHRTFComboBox->setCurrentIndex(toIndex(Settings::sound().mHrtfEnable)); const std::string& selectedHRTFProfile = Settings::sound().mHrtf; if (selectedHRTFProfile.empty() == false) { @@ -224,6 +292,7 @@ bool Launcher::SettingsPage::loadSettings() hrtfProfileSelectorComboBox->setCurrentIndex(hrtfProfileIndex); } } + loadSettingBool(Settings::sound().mCameraListener, *cameraListenerCheckBox); } // Interface Changes @@ -251,7 +320,6 @@ bool Launcher::SettingsPage::loadSettings() // Miscellaneous { // Saves - loadSettingBool(Settings::saves().mTimeplayed, *timePlayedCheckbox); loadSettingInt(Settings::saves().mMaxQuicksaves, *maximumQuicksavesComboBox); // Other Settings @@ -267,7 +335,7 @@ bool Launcher::SettingsPage::loadSettings() { loadSettingBool(Settings::input().mGrabCursor, *grabCursorCheckBox); - bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; + bool skipMenu = mGameSettings.value("skip-menu").value.toInt() == 1; if (skipMenu) { skipMenuCheckBox->setCheckState(Qt::Checked); @@ -275,8 +343,8 @@ bool Launcher::SettingsPage::loadSettings() startDefaultCharacterAtLabel->setEnabled(skipMenu); startDefaultCharacterAtField->setEnabled(skipMenu); - startDefaultCharacterAtField->setText(mGameSettings.value("start")); - runScriptAfterStartupField->setText(mGameSettings.value("script-run")); + startDefaultCharacterAtField->setText(mGameSettings.value("start").value); + runScriptAfterStartupField->setText(mGameSettings.value("script-run").value); } return true; } @@ -327,7 +395,9 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*weaponSheathingCheckBox, Settings::game().mWeaponSheathing); saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing); saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection); + saveSettingBool(*smoothAnimTransitionsCheckBox, Settings::game().mSmoothAnimTransitions); saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement); + saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation); const bool wantDistantLand = distantLandCheckBox->checkState() == Qt::Checked; if (wantDistantLand != (Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging)) @@ -347,6 +417,52 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*exponentialFogCheckBox, Settings::fog().mExponentialFog); saveSettingBool(*skyBlendingCheckBox, Settings::fog().mSkyBlending); Settings::fog().mSkyBlendingStart.set(skyBlendingStartComboBox->value()); + + static constexpr std::array lightingMethodMap = { + SceneUtil::LightingMethod::FFP, + SceneUtil::LightingMethod::PerObjectUniform, + SceneUtil::LightingMethod::SingleUBO, + }; + Settings::shaders().mLightingMethod.set(lightingMethodMap[lightingMethodComboBox->currentIndex()]); + + const int cShadowDist + = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; + Settings::shadows().mMaximumShadowMapDistance.set(cShadowDist); + const float cFadeStart = fadeStartSpinBox->value(); + if (cShadowDist > 0) + Settings::shadows().mShadowFadeStart.set(cFadeStart); + + const bool cActorShadows = actorShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cObjectShadows = objectShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cTerrainShadows = terrainShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cPlayerShadows = playerShadowsCheckBox->checkState() != Qt::Unchecked; + if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) + { + Settings::shadows().mEnableShadows.set(true); + Settings::shadows().mActorShadows.set(cActorShadows); + Settings::shadows().mPlayerShadows.set(cPlayerShadows); + Settings::shadows().mObjectShadows.set(cObjectShadows); + Settings::shadows().mTerrainShadows.set(cTerrainShadows); + } + else + { + Settings::shadows().mEnableShadows.set(false); + Settings::shadows().mActorShadows.set(false); + Settings::shadows().mPlayerShadows.set(false); + Settings::shadows().mObjectShadows.set(false); + Settings::shadows().mTerrainShadows.set(false); + } + + Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked); + Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt()); + + auto index = shadowComputeSceneBoundsComboBox->currentIndex(); + if (index == 0) + Settings::shadows().mComputeSceneBounds.set("bounds"); + else if (index == 1) + Settings::shadows().mComputeSceneBounds.set("primitives"); + else + Settings::shadows().mComputeSceneBounds.set("none"); } // Audio @@ -356,12 +472,20 @@ void Launcher::SettingsPage::saveSettings() else Settings::sound().mDevice.set({}); - Settings::sound().mHrtfEnable.set(enableHRTFComboBox->currentIndex() - 1); + static constexpr std::array hrtfModes{ + Settings::HrtfMode::Auto, + Settings::HrtfMode::Disable, + Settings::HrtfMode::Enable, + }; + Settings::sound().mHrtfEnable.set(hrtfModes[enableHRTFComboBox->currentIndex()]); if (hrtfProfileSelectorComboBox->currentIndex() != 0) Settings::sound().mHrtf.set(hrtfProfileSelectorComboBox->currentText().toStdString()); else Settings::sound().mHrtf.set({}); + + const bool cCameraListener = cameraListenerCheckBox->checkState() != Qt::Unchecked; + Settings::sound().mCameraListener.set(cCameraListener); } // Interface Changes @@ -389,7 +513,6 @@ void Launcher::SettingsPage::saveSettings() // Miscellaneous { // Saves Settings - saveSettingBool(*timePlayedCheckbox, Settings::saves().mTimeplayed); saveSettingInt(*maximumQuicksavesComboBox, Settings::saves().mMaxQuicksaves); // Other Settings @@ -402,17 +525,17 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*grabCursorCheckBox, Settings::input().mGrabCursor); int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; - if (skipMenu != mGameSettings.value("skip-menu").toInt()) - mGameSettings.setValue("skip-menu", QString::number(skipMenu)); + if (skipMenu != mGameSettings.value("skip-menu").value.toInt()) + mGameSettings.setValue("skip-menu", { QString::number(skipMenu) }); QString startCell = startDefaultCharacterAtField->text(); - if (startCell != mGameSettings.value("start")) + if (startCell != mGameSettings.value("start").value) { - mGameSettings.setValue("start", startCell); + mGameSettings.setValue("start", { startCell }); } QString scriptRun = runScriptAfterStartupField->text(); - if (scriptRun != mGameSettings.value("script-run")) - mGameSettings.setValue("script-run", scriptRun); + if (scriptRun != mGameSettings.value("script-run").value) + mGameSettings.setValue("script-run", { scriptRun }); } } @@ -444,3 +567,15 @@ void Launcher::SettingsPage::slotSkyBlendingToggled(bool checked) skyBlendingStartComboBox->setEnabled(checked); skyBlendingStartLabel->setEnabled(checked); } + +void Launcher::SettingsPage::slotShadowDistLimitToggled(bool checked) +{ + shadowDistanceSpinBox->setEnabled(checked); + fadeStartSpinBox->setEnabled(checked); +} + +void Launcher::SettingsPage::slotDistantLandToggled(bool checked) +{ + activeGridObjectPagingCheckBox->setEnabled(checked); + objectPagingMinSizeComboBox->setEnabled(checked); +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 9f7d6b1f43..d2bb80d86a 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -32,6 +32,8 @@ namespace Launcher void slotAnimSourcesToggled(bool checked); void slotPostProcessToggled(bool checked); void slotSkyBlendingToggled(bool checked); + void slotShadowDistLimitToggled(bool checked); + void slotDistantLandToggled(bool checked); private: Config::GameSettings& mGameSettings; diff --git a/files/ui/datafilespage.ui b/apps/launcher/ui/datafilespage.ui similarity index 61% rename from files/ui/datafilespage.ui rename to apps/launcher/ui/datafilespage.ui index 239df34961..65236ca5ca 100644 --- a/files/ui/datafilespage.ui +++ b/apps/launcher/ui/datafilespage.ui @@ -6,8 +6,8 @@ 0 0 - 571 - 384 + 573 + 557 @@ -29,8 +29,14 @@ + + + 0 + 0 + + - <html><head/><body><p><span style=" font-style:italic;">note: content files that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + <html><head/><body><p>Note: content files that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> @@ -41,14 +47,120 @@ Data Directories - + + + true + QAbstractItemView::InternalMove + + QAbstractItemView::ExtendedSelection + + + Qt::CustomContextMenu + - + + + + + + + 0 + 33 + + + + Scan directories for likely data directories and append them at the end of the list. + + + Append + + + + + + + + 0 + 33 + + + + Scan directories for likely data directories and insert them above the selected position + + + Insert Above + + + + + + + + 0 + 33 + + + + Move selected directory one position up + + + Move Up + + + + + + + + 0 + 33 + + + + Move selected directory one position down + + + Move Down + + + + + + + + 0 + 33 + + + + Remove selected directory + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + @@ -57,117 +169,7 @@ - <html><head/><body><p><span style=" font-style:italic;">note: directories that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> - - - - - - - - 0 - 33 - - - - - 0 - 33 - - - - Scan directories for likely data directories and append them at the end of the list. - - - Append - - - - - - - - 0 - 33 - - - - - 0 - 33 - - - - Scan directories for likely data directories and insert them above the selected position - - - Insert Above - - - - - - - - 0 - 33 - - - - - 0 - 33 - - - - Move selected directory one position up - - - Move Up - - - - - - - - 0 - 33 - - - - - 0 - 33 - - - - Move selected directory one position down - - - Move Down - - - - - - - - 0 - 33 - - - - - 0 - 33 - - - - Remove selected directory - - - Remove + <html><head/><body><p>Note: directories that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> @@ -178,61 +180,90 @@ Archive Files - + + + true + + + Qt::CustomContextMenu + QAbstractItemView::InternalMove + + Qt::CopyAction + + + QAbstractItemView::ExtendedSelection + - - - - 0 - 33 - - - - - 0 - 33 - - - - Move selected archive one position up - - - Move Up - - + + + + + + 0 + 33 + + + + + 0 + 33 + + + + Move selected archive one position up + + + Move Up + + + + + + + + 0 + 33 + + + + + 0 + 33 + + + + Move selected archive one position down + + + Move Down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - + - <html><head/><body><p><span style=" font-style:italic;">note: archives that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> - - - - - - - - 0 - 33 - - - - - 0 - 33 - - - - Move selected archive one position down - - - Move Down + <html><head/><body><p>Note: archives that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> @@ -286,7 +317,7 @@ - Remove unused tiles + Remove Unused Tiles true @@ -298,7 +329,7 @@ - Max size + Max Size diff --git a/files/ui/directorypicker.ui b/apps/launcher/ui/directorypicker.ui similarity index 100% rename from files/ui/directorypicker.ui rename to apps/launcher/ui/directorypicker.ui diff --git a/apps/launcher/ui/graphicspage.ui b/apps/launcher/ui/graphicspage.ui new file mode 100644 index 0000000000..b3e2b15e39 --- /dev/null +++ b/apps/launcher/ui/graphicspage.ui @@ -0,0 +1,243 @@ + + + GraphicsPage + + + + 0 + 0 + 650 + 358 + + + + + + + + + + + + Screen + + + + + + + Window Mode + + + + + + + + + + + 800 + + + + + + + × + + + + + + + 600 + + + + + + + + + Custom: + + + + + + + Standard: + + + true + + + + + + + + + + + + + 0 + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + + + + Framerate Limit + + + + + + + Window Border + + + + + + + + + + 0 + + + + Disabled + + + + + Enabled + + + + + Adaptive + + + + + + + + 0 + + + + Fullscreen + + + + + Windowed Fullscreen + + + + + Windowed + + + + + + + + Resolution + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + false + + + FPS + + + 1 + + + 1.000000000000000 + + + 1000.000000000000000 + + + 15.000000000000000 + + + 300.000000000000000 + + + + + + + Anti-Aliasing + + + + + + + Vertical Synchronization + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + diff --git a/files/ui/importpage.ui b/apps/launcher/ui/importpage.ui similarity index 88% rename from files/ui/importpage.ui rename to apps/launcher/ui/importpage.ui index 3e2b0c5e64..0b5d014afa 100644 --- a/files/ui/importpage.ui +++ b/apps/launcher/ui/importpage.ui @@ -6,7 +6,7 @@ 0 0 - 514 + 515 397 @@ -54,7 +54,7 @@ - File to import settings from: + File to Import Settings From: @@ -73,7 +73,7 @@ - Import add-on and plugin selection (creates a new Content List) + Import Add-on and Plugin Selection true @@ -88,7 +88,7 @@ so OpenMW provides another set of fonts to avoid these issues. These fonts use T to default Morrowind fonts. Check this box if you still prefer original fonts over OpenMW ones or if you use custom bitmap fonts.
- Import bitmap fonts setup + Import Bitmap Fonts true @@ -129,16 +129,22 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov + + + + Qt::Vertical + + + + 0 + 0 + + + + - - - - Qt::Vertical - - - diff --git a/files/ui/mainwindow.ui b/apps/launcher/ui/mainwindow.ui similarity index 85% rename from files/ui/mainwindow.ui rename to apps/launcher/ui/mainwindow.ui index 54a369999d..b5ee65d17c 100644 --- a/files/ui/mainwindow.ui +++ b/apps/launcher/ui/mainwindow.ui @@ -6,21 +6,21 @@ 0 0 - 720 - 565 + 775 + 635 - 720 - 565 + 775 + 635 OpenMW Launcher - + :/images/openmw.png:/images/openmw.png @@ -75,20 +75,6 @@ Qt::LeftToRight - - QToolBar { - background-attachment: fixed; - background-image: url(:/images/openmw-header.png); - background-repeat: none; - background-position: right; - border: 0px; -} - -QToolButton { - min-width: 70px; -} - - false @@ -120,7 +106,7 @@ QToolButton { true - + :/images/openmw-plugin.png:/images/openmw-plugin.png @@ -141,14 +127,14 @@ QToolButton { true - + :/images/preferences-video.png:/images/preferences-video.png - Graphics + Display - Allows to change graphics settings + Allows to change display settings @@ -156,7 +142,7 @@ QToolButton { true - + :/images/preferences.png:/images/preferences.png @@ -171,7 +157,7 @@ QToolButton { true - + :/images/preferences-advanced.png:/images/preferences-advanced.png @@ -183,7 +169,7 @@ QToolButton { - + diff --git a/apps/launcher/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui new file mode 100644 index 0000000000..e792ac2843 --- /dev/null +++ b/apps/launcher/ui/settingspage.ui @@ -0,0 +1,1643 @@ + + + SettingsPage + + + + 0 + 0 + 741 + 503 + + + + + + + 0 + + + + Gameplay + + + + + + + + <html><head/><body><p>Make Damage Fatigue magic effect uncapped like Drain Fatigue effect.</p><p>This means that unlike Morrowind you will be able to knock down actors using this effect.</p></body></html> + + + Uncapped Damage Fatigue + + + + + + + <html><head/><body><p>Give actors an ability to swim over the water surface when they follow other actor independently from their ability to swim. Has effect only when nav mesh building is enabled.</p></body></html> + + + Always Allow Actors to Follow over Water + + + + + + + <html><head/><body><p>Make disposition change of merchants caused by trading permanent.</p></body></html> + + + Permanent Barter Disposition Changes + + + + + + + <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> + + + Racial Variation in Speed Fix + + + + + + + <html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html> + + + Classic Calm Spells Behavior + + + + + + + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + + + NPCs Avoid Collisions + + + + + + + <html><head/><body><p>Make the value of filled soul gems dependent only on soul magnitude.</p></body></html> + + + Soulgem Values Rebalance + + + + + + + <html><head/><body><p>If this setting is true, supporting models will make use of day night switch nodes.</p></body></html> + + + Day Night Switch Nodes + + + + + + + <html><head/><body><p>Make player followers and escorters start combat with enemies who have started combat with them or the player. Otherwise they wait for the enemies or the player to do an attack first.</p></body></html> + + + Followers Defend Immediately + + + + + + + <html><head/><body><p><a name="docs-internal-guid-f375b85a-7fff-02ff-a5af-c5cff63923c0"/>When enabled, a navigation mesh is built in the background for world geometry to be used for pathfinding. When disabled only the path grid is used to build paths. Single-core CPU systems may have a big performance impact on existing interior location and moving across the exterior world. May slightly affect performance on multi-core CPU systems. Multi-core CPU systems may have different latency for nav mesh update depending on other settings and system performance. Moving across external world, entering/exiting location produce nav mesh update. NPC and creatures may not be able to find path before nav mesh is built around them. Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and cast a firebolt.</p></body></html> + + + Use Navigation Mesh for Pathfinding + + + + + + + <html><head/><body><p>If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.</p></body></html> + + + Only Magical Ammo Bypass Resistance + + + + + + + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> + + + Graphic Herbalism + + + + + + + <html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html> + + + Swim Upward Correction + + + + + + + <html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> + + + Enchanted Weapons Are Magical + + + + + + + <html><head/><body><p>Prevents merchants from equipping items that are sold to them.</p></body></html> + + + Merchant Equipping Fix + + + + + + + <html><head/><body><p>Trainers now only choose which skills to train using their base skill points, allowing mercantile improving effects to be used without making mercantile an offered skill.</p></body></html> + + + Trainers Choose Offered Skills by Base Value + + + + + + + <html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html> + + + Can Loot During Death Animation + + + + + + + <html><head/><body><p>Make stealing items from NPCs that were knocked down possible during combat.</p></body></html> + + + Steal from Knocked out Actors in Combat + + + + + + + <html><head/><body><p>Effects of reflected Absorb spells are not mirrored - like in Morrowind.</p></body></html> + + + Classic Reflected Absorb Spells Behavior + + + + + + + <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> + + + Unarmed Creature Attacks Damage Armor + + + + + + + + + + + <html><head/><body><p>This setting controls the behavior of factoring of Strength attribute into hand-to-hand damage: damage is multiplied by Strength value divided by 40.</p><p>Can apply to all actors or only to non-werewolf actors.</p></body></html> + + + Factor Strength into Hand-to-Hand Combat + + + + + + + 0 + + + + Off + + + + + Affect Werewolves + + + + + Do Not Affect Werewolves + + + + + + + + <html><head/><body><p>How many threads will be spawned to compute physics update in the background. A value of 0 means that the update will be performed in the main thread.</p><p>A value greater than 1 requires the Bullet library be compiled with multithreading support.</p></body></html> + + + Background Physics Threads + + + + + + + + + + Actor Collision Shape Type + + + + + + + Collision is used for both physics simulation and navigation mesh generation for pathfinding. Cylinder gives the best consistency between available navigation paths and ability to move by them. Changing this value affects navigation mesh generation therefore navigation mesh disk cache generated for one value will not be useful with another. + + + Axis-Aligned Bounding Box + + + + Axis-Aligned Bounding Box + + + + + Rotating Box + + + + + Cylinder + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Visuals + + + + + + + + 0 + + + + Animations + + + + + + + + <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> + + + Smooth Movement + + + + + + + <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html> + + + Use Additional Animation Sources + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Affects side and diagonal movement. Enabling this setting makes movement more realistic.</p><p>If disabled then the whole character's body is pointed to the direction of view. Diagonal movement has no special animation and causes sliding.</p><p>If enabled then the character turns lower body to the direction of movement. Upper body is turned partially. Head is always pointed to the direction of view. In combat mode it works only for diagonal movement. In non-combat mode it changes straight right and straight left movement as well. Also turns the whole body up or down when swimming according to the movement direction.</p></body></html> + + + Turn to Movement Direction + + + + + + + false + + + <html><head/><body><p>Render holstered weapons (with quivers and scabbards), requires modded assets.</p></body></html> + + + Weapon Sheathing + + + + + + + false + + + <html><head/><body><p>Render holstered shield, requires modded assets.</p></body></html> + + + Shield Sheathing + + + + + + + <html><head/><body><p>In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. This was the default behavior of OpenMW 0.48 and earlier.</p></body></html> + + + Player Movement Ignores Animation + + + + + + + <html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html> + + + Use Magic Item Animation + + + + + + + <html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html> + + + Smooth Animation Transitions + + + + + + + + + + Shaders + + + + + + + + + + <html><head/><body><p>If this option is enabled, normal maps are automatically recognized and used if they are named appropriately + (see 'normal map pattern', e.g. for a base texture foo.dds, the normal map texture would have to be named foo_n.dds). + If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.nif or .osg file). Affects objects.</p></body></html> + + + Auto Use Object Normal Maps + + + + + + + <html><head/><body><p>Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.</p></body></html> + + + Soft Particles + + + + + + + <html><head/><body><p>If this option is enabled, specular maps are automatically recognized and used if they are named appropriately + (see 'specular map pattern', e.g. for a base texture foo.dds, + the specular map texture would have to be named foo_spec.dds). + If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file + (.osg file, not supported in .nif files). Affects objects.</p></body></html> + + + Auto Use Object Specular Maps + + + + + + + <html><head/><body><p>See 'auto use object normal maps'. Affects terrain.</p></body></html> + + + Auto Use Terrain Normal Maps + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>If a file with pattern 'terrain specular map pattern' exists, use that file as a 'diffuse specular' map. The texture must contain the layer colour in the RGB channel (as usual), and a specular multiplier in the alpha channel.</p></body></html> + + + Auto Use Terrain Specular Maps + + + + + + + <html><head/><body><p>Simulate coverage-preserving mipmaps to prevent alpha-tested meshes shrinking as they get further away. Will cause meshes whose textures have coverage-preserving mipmaps to grow, though, so refer to mod installation instructions for how to set this.</p></body></html> + + + Adjust Coverage for Alpha Test + + + + + + + <html><head/><body><p>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> + + + Use Anti-Aliased Alpha Testing + + + + + + + <html><head/><body><p>Normally environment map reflections aren't affected by lighting, which makes environment-mapped (and thus bump-mapped objects) glow in the dark. + Morrowind Code Patch includes an option to remedy that by doing environment-mapping before applying lighting, this is the equivalent of that option. + Affected objects will use shaders. + </p></body></html> + + + Bump/Reflect Map Local Lighting + + + + + + + <html><head/><body><p>EXPERIMENTAL: Stop rain and snow from falling through overhangs and roofs.</p></body></html> + + + Weather Particle Occlusion + + + + + + + + + + + + Fog + + + + + + + + <html><head/><body><p>Use exponential fog formula. By default, linear fog is used.</p></body></html> + + + Exponential Fog + + + + + + + false + + + 3 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.005000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>By default, the fog becomes thicker proportionally to your distance from the clipping plane set at the clipping distance, which causes distortion at the edges of the screen. + This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV.</p></body></html> + + + Radial Fog + + + + + + + false + + + <html><head/><body><p>The fraction of the maximum distance at which blending with the sky starts.</p></body></html> + + + Sky Blending Start + + + + + + + <html><head/><body><p>Reduce visibility of clipping plane by blending objects with the sky.</p></body></html> + + + Sky Blending + + + + + + + + + + Terrain + + + + + + + + <html><head/><body><p>Controls how large an object must be to be visible in the scene. The object’s size is divided by its distance to the camera and the result of the division is compared with this value. The smaller this value is, the more objects you will see in the scene.</p></body></html> + + + Object Paging Min Size + + + + + + + 3 + + + 0.000000000000000 + + + 0.250000000000000 + + + 0.005000000000000 + + + + + + + Viewing Distance + + + + + + + cells + + + 3 + + + 0.250000000000000 + + + 0.125000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>If true, use paging and LOD algorithms to display the entire terrain. If false, only display terrain of the loaded cells.</p></body></html> + + + Distant Land + + + + + + + <html><head/><body><p>Use object paging for active cells grid.</p></body></html> + + + Active Grid Object Paging + + + + + + + + + + Post Processing + + + + + + + + false + + + 3 + + + 0.010000000000000 + + + 10.000000000000000 + + + 0.001000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + false + + + <html><head/><body><p>Re-render transparent objects with forced alpha clipping.</p></body></html> + + + Transparent Postpass + + + + + + + false + + + <html><head/><body><p>Controls how much eye adaptation can change from frame to frame. Smaller values makes for slower transitions.</p></body></html> + + + Auto Exposure Speed + + + + + + + <html><head/><body><p>If this setting is true, post processing will be enabled.</p></body></html> + + + Enable Post Processing + + + + + + + + + + Shadows + + + + + + + + + Bounds + + + + + Primitives + + + + + None + + + + + + + + <html><head/><body><p>Type of "compute scene bounds" computation method to be used. Bounds (default) for good balance between performance and shadow quality, primitives for better looking shadows or none for no computation.</p></body></html> + + + Shadow Planes Computation Method + + + + + + + false + + + <html><head/><body><p>64 game units is 1 real life yard or about 0.9 m</p></body></html> + + + unit(s) + + + 512 + + + 81920 + + + 128 + + + 8192 + + + + + + + <html><head/><body><p>Enable shadows for NPCs and creatures besides the player character. May have a minor performance impact.</p></body></html> + + + Enable Actor Shadows + + + + + + + + 512 + + + + + 1024 + + + + + 2048 + + + + + 4096 + + + + + + + + <html><head/><body><p>The fraction of the limit above at which shadows begin to gradually fade away.</p></body></html> + + + Fade Start Multiplier + + + + + + + <html><head/><body><p>Enable shadows exclusively for the player character. May have a very minor performance impact.</p></body></html> + + + Enable Player Shadows + + + + + + + <html><head/><body><p>The resolution of each individual shadow map. Increasing it significantly improves shadow quality but may have a minor performance impact.</p></body></html> + + + Shadow Map Resolution + + + + + + + <html><head/><body><p>The distance from the camera at which shadows completely disappear.</p></body></html> + + + Shadow Distance Limit: + + + + + + + <html><head/><body><p>Enable shadows for primarily inanimate objects. May have a significant performance impact.</p></body></html> + + + Enable Object Shadows + + + + + + + false + + + 2 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + 0.900000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Due to limitations with Morrowind's data, only actors can cast shadows indoors, which some might feel is distracting.</p><p>Has no effect if actor/player shadows are not enabled.</p></body></html> + + + Enable Indoor Shadows + + + + + + + <html><head/><body><p>Enable shadows for the terrain including distant terrain. May have a significant performance and shadow quality impact.</p></body></html> + + + Enable Terrain Shadows + + + + + + + + + + Lighting + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Set the internal handling of light sources.</p> +<p> "Legacy" always uses 8 lights per object. It provides results most similar to Morrowind's lighting.</p> +<p>"Shaders (compatibility)" removes the 8 light limit. This mode also enables lighting on groundcover. It is recommended to use this with older hardware and a light limit closer to 8.</p> +<p> "Shaders" carries all of the benefits that "Shaders (compatibility)" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware.</p></body></html> + + + Lighting Method + + + + + + + + Legacy + + + + + Shaders (compatibility) + + + + + Shaders + + + + + + + + + + + + + + + + + Audio + + + + + + + + Select your preferred audio device. + + + Audio Device + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Default + + + + + + + + + + + + This setting controls HRTF, which simulates 3D sound on stereo systems. + + + HRTF + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Automatic + + + + + Off + + + + + On + + + + + + + + + + + + Select your preferred HRTF profile. + + + HRTF Profile + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Default + + + + + + + + + + In third-person view, use the camera as the sound listener instead of the player character. + + + Use the Camera as the Sound Listener + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + Interface + + + + + + + + 1 + + + + Off + + + + + Tooltip + + + + + Crosshair + + + + + Tooltip and Crosshair + + + + + + + + 2 + + + 0.500000000000000 + + + 8.000000000000000 + + + 0.250000000000000 + + + 1.000000000000000 + + + + + + + 12 + + + 18 + + + 1 + + + 16 + + + + + + + <html><head/><body><p>This setting scales GUI windows. A value of 1.0 results in the normal scale.</p></body></html> + + + GUI Scaling Factor + + + + + + + <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> + + + Show Effect Duration + + + + + + + <html><head/><body><p>If this setting is true, dialogue topics will have a different color if the topic is specific to the NPC you're talking to or the topic was previously seen. Color can be changed in settings.cfg.</p><p>The default value is false.</p></body></html> + + + Change Dialogue Topic Color + + + + + + + Size of characters in game texts. + + + Font Size + + + + + + + <html><head/><body><p>Enable zooming on local and global maps.</p></body></html> + + + Can Zoom on Maps + + + + + + + <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be shown on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show Projectile Damage + + + + + + + <html><head/><body><p>If this setting is true, melee weapons reach and speed will be shown on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show Melee Info + + + + + + + <html><head/><body><p>Stretch menus, load screens, etc. to the window aspect ratio.</p></body></html> + + + Stretch Menu Background + + + + + + + Show Owned Objects + + + + + + + <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> + + + Show Enchant Chance + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Miscellaneous + + + + + + Saves + + + + + + + + Maximum Quicksaves + + + + + + + 1 + + + + + + + + + + + + Screenshots + + + + + + + + Screenshot Format + + + + + + + + JPG + + + + + PNG + + + + + TGA + + + + + + + + + + Notify on Saved Screenshot + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + Testing + + + + + + These settings are intended for testing mods and will cause issues if used for normal gameplay. + + + true + + + + + + + Qt::Horizontal + + + + + + + <html><head/><body><p>OpenMW will capture control of the cursor if this setting is true.</p><p>In “look mode”, OpenMW will center the cursor regardless of the value of this setting (since the cursor/crosshair is always centered in the OpenMW window). However, in GUI mode, this setting determines the behavior when the cursor is moved outside the OpenMW window. If true, the cursor movement stops at the edge of the window preventing access to other applications. If false, the cursor is allowed to move freely on the desktop.</p><p>This setting does not apply to the screen where escape has been pressed, where the cursor is never captured. Regardless of this setting “Alt-Tab” or some other operating system dependent key sequence can be used to allow the operating system to regain control of the mouse cursor. This setting interacts with the minimize on focus loss setting by affecting what counts as a focus loss. Specifically on a two-screen configuration it may be more convenient to access the second screen with setting disabled.</p><p>Note for developers: it’s desirable to have this setting disabled when running the game in a debugger, to prevent the mouse cursor from becoming unusable when the game pauses on a breakpoint.</p></body></html> + + + Grab Cursor + + + + + + + Skip Menu and Generate Default Character + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 0 + 0 + + + + + + + + Start Default Character at + + + + + + + Default Cell + + + + + + + + + Run Script After Startup: + + + + + + + + + + + + Browse… + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + + diff --git a/apps/launcher/utils/lineedit.cpp b/apps/launcher/utils/lineedit.cpp index 0741e84c12..ad3f82ec30 100644 --- a/apps/launcher/utils/lineedit.cpp +++ b/apps/launcher/utils/lineedit.cpp @@ -11,7 +11,7 @@ void LineEdit::setupClearButton() mClearButton = new QToolButton(this); mClearButton->setIcon(QIcon::fromTheme("edit-clear")); mClearButton->setCursor(Qt::ArrowCursor); - mClearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + mClearButton->setAutoRaise(true); mClearButton->hide(); connect(mClearButton, &QToolButton::clicked, this, &LineEdit::clear); connect(this, &LineEdit::textChanged, this, &LineEdit::updateClearButton); diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp index 1a332e9788..9a9ae9981b 100644 --- a/apps/launcher/utils/openalutil.cpp +++ b/apps/launcher/utils/openalutil.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include "apps/openmw/mwsound/alext.h" #include "openalutil.hpp" diff --git a/apps/mwiniimporter/CMakeLists.txt b/apps/mwiniimporter/CMakeLists.txt index 704393cd0d..ed88ac0bc4 100644 --- a/apps/mwiniimporter/CMakeLists.txt +++ b/apps/mwiniimporter/CMakeLists.txt @@ -14,13 +14,11 @@ openmw_add_executable(openmw-iniimporter ) target_link_libraries(openmw-iniimporter - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options components ) if (WIN32) - target_link_libraries(openmw-iniimporter - ${Boost_LOCALE_LIBRARY}) INSTALL(TARGETS openmw-iniimporter RUNTIME DESTINATION ".") endif(WIN32) @@ -33,7 +31,7 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(openmw-iniimporter gcov) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-iniimporter PRIVATE diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 7b3bcc3f1c..8c7c238b4a 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -584,7 +584,7 @@ void MwIniImporter::importGameFiles( reader.close(); } - auto sortedFiles = dependencySort(unsortedFiles); + auto sortedFiles = dependencySort(std::move(unsortedFiles)); // hard-coded dependency Morrowind - Tribunal - Bloodmoon if (findString(sortedFiles, "Morrowind.esm") != sortedFiles.end()) diff --git a/apps/navmeshtool/CMakeLists.txt b/apps/navmeshtool/CMakeLists.txt index 9abd8dc283..090fc00f36 100644 --- a/apps/navmeshtool/CMakeLists.txt +++ b/apps/navmeshtool/CMakeLists.txt @@ -5,10 +5,22 @@ set(NAVMESHTOOL ) source_group(apps\\navmeshtool FILES ${NAVMESHTOOL}) -openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL}) +add_library(openmw-navmeshtool-lib STATIC + ${NAVMESHTOOL} +) -target_link_libraries(openmw-navmeshtool - ${Boost_PROGRAM_OPTIONS_LIBRARY} +if (ANDROID) + add_library(openmw-navmeshtool SHARED + main.cpp + ) +else() + openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL}) +endif() + +target_link_libraries(openmw-navmeshtool openmw-navmeshtool-lib) + +target_link_libraries(openmw-navmeshtool-lib + Boost::program_options components ) @@ -21,7 +33,7 @@ if (WIN32) install(TARGETS openmw-navmeshtool RUNTIME DESTINATION ".") endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-navmeshtool PRIVATE diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index e5126d9209..27f84104ac 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -88,10 +89,6 @@ namespace NavMeshTool ->composing(), "set fallback BSA archives (later archives have higher priority)"); - addOption("resources", - bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), - "set resources directory"); - addOption("content", bpo::value()->default_value(StringsVector(), "")->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts"); @@ -143,14 +140,14 @@ namespace NavMeshTool if (variables.find("help") != variables.end()) { - getRawStdout() << desc << std::endl; + Debug::getRawStdout() << desc << std::endl; return 0; } Files::ConfigurationManager config; config.readConfiguration(variables, desc); - setupLogging(config.getLogPath(), applicationName); + Debug::setupLogging(config.getLogPath(), applicationName); const std::string encoding(variables["encoding"].as()); Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); @@ -164,13 +161,14 @@ namespace NavMeshTool config.filterOutNonExistingPaths(dataDirs); - const auto resDir = variables["resources"].as(); - Version::Version v = Version::getOpenmwVersion(resDir); - Log(Debug::Info) << v.describe(); + const auto& resDir = variables["resources"].as(); + Log(Debug::Info) << Version::getOpenmwVersionDescription(); dataDirs.insert(dataDirs.begin(), resDir / "vfs"); - const auto fileCollections = Files::Collections(dataDirs); - const auto archives = variables["fallback-archive"].as(); - const auto contentFiles = variables["content"].as(); + const Files::Collections fileCollections(dataDirs); + const auto& archives = variables["fallback-archive"].as(); + StringsVector contentFiles{ "builtin.omwscripts" }; + const auto& configContentFiles = variables["content"].as(); + contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end()); const std::size_t threadsNumber = variables["threads"].as(); if (threadsNumber < 1) @@ -200,7 +198,7 @@ namespace NavMeshTool Settings::game().mActorCollisionShapeType, Settings::game().mDefaultActorPathfindHalfExtents, }; - const std::uint64_t maxDbFileSize = Settings::Manager::getUInt64("max navmeshdb file size", "Navigator"); + const std::uint64_t maxDbFileSize = Settings::navigator().mMaxNavmeshdbFileSize; const auto dbPath = Files::pathToUnicodeString(config.getUserDataPath() / "navmesh.db"); Log(Debug::Info) << "Using navmeshdb at " << dbPath; @@ -219,10 +217,13 @@ namespace NavMeshTool const EsmLoader::EsmData esmData = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder); - Resource::ImageManager imageManager(&vfs); - Resource::NifFileManager nifFileManager(&vfs); - Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager); - Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager); + constexpr double expiryDelay = 0; + + Resource::ImageManager imageManager(&vfs, expiryDelay); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); + Resource::BgsmFileManager bgsmFileManager(&vfs, expiryDelay); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); + Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); DetourNavigator::RecastGlobalAllocator::init(); DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); navigatorSettings.mRecast.mSwimHeightScale @@ -257,7 +258,11 @@ namespace NavMeshTool } } +#ifdef ANDROID +extern "C" int SDL_main(int argc, char* argv[]) +#else int main(int argc, char* argv[]) +#endif { - return wrapApplication(NavMeshTool::runNavMeshTool, argc, argv, NavMeshTool::applicationName); + return Debug::wrapApplication(NavMeshTool::runNavMeshTool, argc, argv, NavMeshTool::applicationName); } diff --git a/apps/navmeshtool/navmesh.cpp b/apps/navmeshtool/navmesh.cpp index 384c965466..6a4a708ef9 100644 --- a/apps/navmeshtool/navmesh.cpp +++ b/apps/navmeshtool/navmesh.cpp @@ -59,7 +59,7 @@ namespace NavMeshTool void serializeToStderr(const T& value) { const std::vector data = serialize(value); - getLockedRawStderr()->write( + Debug::getLockedRawStderr()->write( reinterpret_cast(data.data()), static_cast(data.size())); } @@ -106,8 +106,8 @@ namespace NavMeshTool return DetourNavigator::resolveMeshSource(mDb, source, mNextShapeId); } - std::optional find(std::string_view worldspace, const TilePosition& tilePosition, - const std::vector& input) override + std::optional find( + ESM::RefId worldspace, const TilePosition& tilePosition, const std::vector& input) override { std::optional result; std::lock_guard lock(mMutex); @@ -121,7 +121,7 @@ namespace NavMeshTool return result; } - void ignore(std::string_view worldspace, const TilePosition& tilePosition) override + void ignore(ESM::RefId worldspace, const TilePosition& tilePosition) override { if (mRemoveUnusedTiles) { @@ -131,7 +131,7 @@ namespace NavMeshTool report(); } - void identity(std::string_view worldspace, const TilePosition& tilePosition, std::int64_t tileId) override + void identity(ESM::RefId worldspace, const TilePosition& tilePosition, std::int64_t tileId) override { if (mRemoveUnusedTiles) { @@ -142,7 +142,7 @@ namespace NavMeshTool report(); } - void insert(std::string_view worldspace, const TilePosition& tilePosition, std::int64_t version, + void insert(ESM::RefId worldspace, const TilePosition& tilePosition, std::int64_t version, const std::vector& input, PreparedNavMeshData& data) override { { @@ -158,7 +158,7 @@ namespace NavMeshTool report(); } - void update(std::string_view worldspace, const TilePosition& tilePosition, std::int64_t tileId, + void update(ESM::RefId worldspace, const TilePosition& tilePosition, std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) override { data.mUserId = static_cast(tileId); @@ -217,7 +217,7 @@ namespace NavMeshTool mDb.vacuum(); } - void removeTilesOutsideRange(std::string_view worldspace, const TilesPositionsRange& range) + void removeTilesOutsideRange(ESM::RefId worldspace, const TilesPositionsRange& range) { const std::lock_guard lock(mMutex); mTransaction.commit(); diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp index 8d2ac7382b..ec44d33e6c 100644 --- a/apps/navmeshtool/worldspacedata.cpp +++ b/apps/navmeshtool/worldspacedata.cpp @@ -103,8 +103,7 @@ namespace NavMeshTool Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs"; - const auto getKey - = [](const EsmLoader::Record& v) -> const ESM::RefNum& { return v.mValue.mRefNum; }; + const auto getKey = [](const EsmLoader::Record& v) -> ESM::RefNum { return v.mValue.mRefNum; }; std::vector result = prepareRecords(cellRefs, getKey); Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs"; @@ -122,7 +121,7 @@ namespace NavMeshTool for (CellRef& cellRef : cellRefs) { - std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + VFS::Path::Normalized model(getModel(esmData, cellRef.mRefId, cellRef.mType)); if (model.empty()) continue; @@ -132,7 +131,7 @@ namespace NavMeshTool osg::ref_ptr shape = [&] { try { - return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model, &vfs)); + return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model)); } catch (const std::exception& e) { @@ -220,7 +219,8 @@ namespace NavMeshTool void serializeToStderr(const T& value) { const std::vector data = serialize(value); - getRawStderr().write(reinterpret_cast(data.data()), static_cast(data.size())); + Debug::getRawStderr().write( + reinterpret_cast(data.data()), static_cast(data.size())); } std::string makeAddObjectErrorMessage( @@ -235,8 +235,8 @@ namespace NavMeshTool } WorldspaceNavMeshInput::WorldspaceNavMeshInput( - std::string worldspace, const DetourNavigator::RecastSettings& settings) - : mWorldspace(std::move(worldspace)) + ESM::RefId worldspace, const DetourNavigator::RecastSettings& settings) + : mWorldspace(worldspace) , mTileCachedRecastMeshManager(settings) { mAabb.m_min = btVector3(0, 0, 0); @@ -249,7 +249,7 @@ namespace NavMeshTool { Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells..."; - std::map> navMeshInputs; + std::unordered_map> navMeshInputs; WorldspaceData data; std::size_t objectsCounter = 0; @@ -277,8 +277,7 @@ namespace NavMeshTool const osg::Vec2i cellPosition(cell.mData.mX, cell.mData.mY); const std::size_t cellObjectsBegin = data.mObjects.size(); - const auto cellWorldspace = Misc::StringUtils::lowerCase( - (cell.isExterior() ? ESM::Cell::sDefaultWorldspaceId : cell.mId).serializeText()); + const ESM::RefId cellWorldspace = cell.isExterior() ? ESM::Cell::sDefaultWorldspaceId : cell.mId; WorldspaceNavMeshInput& navMeshInput = [&]() -> WorldspaceNavMeshInput& { auto it = navMeshInputs.find(cellWorldspace); if (it == navMeshInputs.end()) diff --git a/apps/navmeshtool/worldspacedata.hpp b/apps/navmeshtool/worldspacedata.hpp index b6ccce6733..7096cf95ed 100644 --- a/apps/navmeshtool/worldspacedata.hpp +++ b/apps/navmeshtool/worldspacedata.hpp @@ -48,12 +48,12 @@ namespace NavMeshTool struct WorldspaceNavMeshInput { - std::string mWorldspace; + ESM::RefId mWorldspace; TileCachedRecastMeshManager mTileCachedRecastMeshManager; btAABB mAabb; bool mAabbInitialized = false; - explicit WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings); + explicit WorldspaceNavMeshInput(ESM::RefId worldspace, const DetourNavigator::RecastSettings& settings); }; class BulletObject diff --git a/apps/niftest/CMakeLists.txt b/apps/niftest/CMakeLists.txt index f112e087e3..cf37162f6e 100644 --- a/apps/niftest/CMakeLists.txt +++ b/apps/niftest/CMakeLists.txt @@ -17,6 +17,6 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(niftest gcov) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(niftest PRIVATE ) endif() diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 06f2110e69..8634134665 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -17,113 +18,213 @@ #include #include #include +#include #include // Create local aliases for brevity namespace bpo = boost::program_options; -/// See if the file has the named extension -bool hasExtension(const std::filesystem::path& filename, const std::string& extensionToFind) +enum class FileType { - const auto extension = Files::pathToUnicodeString(filename.extension()); - return Misc::StringUtils::ciEqual(extension, extensionToFind); + BSA, + BA2, + BGEM, + BGSM, + NIF, + KF, + BTO, + BTR, + RDT, + PSA, + Unknown, +}; + +enum class FileClass +{ + Archive, + Material, + NIF, + Unknown, +}; + +std::pair classifyFile(const std::filesystem::path& filename) +{ + const std::string extension = Misc::StringUtils::lowerCase(Files::pathToUnicodeString(filename.extension())); + if (extension == ".bsa") + return { FileType::BSA, FileClass::Archive }; + if (extension == ".ba2") + return { FileType::BA2, FileClass::Archive }; + if (extension == ".bgem") + return { FileType::BGEM, FileClass::Material }; + if (extension == ".bgsm") + return { FileType::BGSM, FileClass::Material }; + if (extension == ".nif") + return { FileType::NIF, FileClass::NIF }; + if (extension == ".kf") + return { FileType::KF, FileClass::NIF }; + if (extension == ".bto") + return { FileType::BTO, FileClass::NIF }; + if (extension == ".btr") + return { FileType::BTR, FileClass::NIF }; + if (extension == ".rdt") + return { FileType::RDT, FileClass::NIF }; + if (extension == ".psa") + return { FileType::PSA, FileClass::NIF }; + + return { FileType::Unknown, FileClass::Unknown }; } -/// See if the file has the "nif" extension. -bool isNIF(const std::filesystem::path& filename) +std::string getFileTypeName(FileType fileType) { - return hasExtension(filename, ".nif"); -} -/// See if the file has the "bsa" extension. -bool isBSA(const std::filesystem::path& filename) -{ - return hasExtension(filename, ".bsa"); -} - -std::unique_ptr makeBsaArchive(const std::filesystem::path& path) -{ - switch (Bsa::BSAFile::detectVersion(path)) + switch (fileType) { - case Bsa::BSAVER_UNKNOWN: - std::cerr << '"' << path << "\" is unknown BSA archive" << std::endl; - return nullptr; - case Bsa::BSAVER_COMPRESSED: - return std::make_unique::type>(path); - case Bsa::BSAVER_BA2_GNRL: - return std::make_unique::type>(path); - case Bsa::BSAVER_BA2_DX10: - return std::make_unique::type>(path); - case Bsa::BSAVER_UNCOMPRESSED: - return std::make_unique::type>(path); + case FileType::BSA: + return "BSA"; + case FileType::BA2: + return "BA2"; + case FileType::BGEM: + return "BGEM"; + case FileType::BGSM: + return "BGSM"; + case FileType::NIF: + return "NIF"; + case FileType::KF: + return "KF"; + case FileType::BTO: + return "BTO"; + case FileType::BTR: + return "BTR"; + case FileType::RDT: + return "RDT"; + case FileType::PSA: + return "PSA"; + case FileType::Unknown: + default: + return {}; } +} - std::cerr << '"' << path << "\" is unsupported BSA archive" << std::endl; - - return nullptr; +bool isBSA(const std::filesystem::path& path) +{ + return classifyFile(path).second == FileClass::Archive; } std::unique_ptr makeArchive(const std::filesystem::path& path) { if (isBSA(path)) - return makeBsaArchive(path); + return VFS::makeBsaArchive(path); if (std::filesystem::is_directory(path)) return std::make_unique(path); return nullptr; } +bool readFile( + const std::filesystem::path& source, const std::filesystem::path& path, const VFS::Manager* vfs, bool quiet) +{ + const auto [fileType, fileClass] = classifyFile(path); + if (fileClass != FileClass::NIF && fileClass != FileClass::Material) + return false; + + const std::string pathStr = Files::pathToUnicodeString(path); + if (!quiet) + { + std::cout << "Reading " << getFileTypeName(fileType) << " file '" << pathStr << "'"; + if (!source.empty()) + std::cout << " from '" << Files::pathToUnicodeString(isBSA(source) ? source.filename() : source) << "'"; + std::cout << std::endl; + } + const std::filesystem::path fullPath = !source.empty() ? source / path : path; + try + { + switch (fileClass) + { + case FileClass::NIF: + { + Nif::NIFFile file(VFS::Path::Normalized(Files::pathToUnicodeString(fullPath))); + Nif::Reader reader(file, nullptr); + if (vfs != nullptr) + reader.parse(vfs->get(pathStr)); + else + reader.parse(Files::openConstrainedFileStream(fullPath)); + break; + } + case FileClass::Material: + { + if (vfs != nullptr) + Bgsm::parse(vfs->get(pathStr)); + else + Bgsm::parse(Files::openConstrainedFileStream(fullPath)); + break; + } + default: + break; + } + } + catch (std::exception& e) + { + std::cerr << "Failed to read '" << pathStr << "':" << std::endl << e.what() << std::endl; + } + return true; +} + /// Check all the nif files in a given VFS::Archive /// \note Can not read a bsa file inside of a bsa file. -void readVFS(std::unique_ptr&& anArchive, const std::filesystem::path& archivePath = {}) +void readVFS(std::unique_ptr&& archive, const std::filesystem::path& archivePath, bool quiet) { - if (anArchive == nullptr) + if (archive == nullptr) return; - VFS::Manager myManager; - myManager.addArchive(std::move(anArchive)); - myManager.buildIndex(); + if (!quiet) + std::cout << "Reading data source '" << Files::pathToUnicodeString(archivePath) << "'" << std::endl; - for (const auto& name : myManager.getRecursiveDirectoryIterator("")) + VFS::Manager vfs; + vfs.addArchive(std::move(archive)); + vfs.buildIndex(); + + for (const auto& name : vfs.getRecursiveDirectoryIterator()) { - try + readFile(archivePath, name.value(), &vfs, quiet); + } + + if (!archivePath.empty() && !isBSA(archivePath)) + { + const Files::Collections fileCollections({ archivePath }); + const Files::MultiDirCollection& bsaCol = fileCollections.getCollection(".bsa"); + const Files::MultiDirCollection& ba2Col = fileCollections.getCollection(".ba2"); + for (const Files::MultiDirCollection& collection : { bsaCol, ba2Col }) { - if (isNIF(name)) + for (auto& file : collection) { - // std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile file(archivePath / name); - Nif::Reader reader(file); - reader.parse(myManager.get(name)); - } - else if (isBSA(name)) - { - if (!archivePath.empty() && !isBSA(archivePath)) + try { - // std::cout << "Reading BSA File: " << name << std::endl; - readVFS(makeBsaArchive(archivePath / name), archivePath / name); - // std::cout << "Done with BSA File: " << name << std::endl; + readVFS(VFS::makeBsaArchive(file.second), file.second, quiet); + } + catch (const std::exception& e) + { + std::cerr << "Failed to read archive file '" << Files::pathToUnicodeString(file.second) + << "': " << e.what() << std::endl; } } } - catch (std::exception& e) - { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; - } } } -bool parseOptions(int argc, char** argv, std::vector& files, bool& writeDebugLog, - std::vector& archives) +bool parseOptions(int argc, char** argv, Files::PathContainer& files, Files::PathContainer& archives, + bool& writeDebugLog, bool& quiet) { - bpo::options_description desc(R"(Ensure that OpenMW can use the provided NIF and BSA files + bpo::options_description desc( + R"(Ensure that OpenMW can use the provided NIF, KF, BTO/BTR, RDT, PSA, BGEM/BGSM and BSA/BA2 files Usages: - niftool - Scan the file or directories for nif errors. + niftest + Scan the file or directories for NIF errors. Allowed options)"); auto addOption = desc.add_options(); addOption("help,h", "print help message."); addOption("write-debug-log,v", "write debug log for unsupported nif files"); + addOption("quiet,q", "do not log read archives/files"); addOption("archives", bpo::value(), "path to archive files to provide files"); addOption("input-file", bpo::value(), "input file"); @@ -143,17 +244,18 @@ Allowed options)"); return false; } writeDebugLog = variables.count("write-debug-log") > 0; + quiet = variables.count("quiet") > 0; if (variables.count("input-file")) { - files = variables["input-file"].as(); + files = asPathContainer(variables["input-file"].as()); if (const auto it = variables.find("archives"); it != variables.end()) - archives = it->second.as(); + archives = asPathContainer(it->second.as()); return true; } } catch (std::exception& e) { - std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" << desc << std::endl; + std::cout << "Error parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } @@ -164,64 +266,63 @@ Allowed options)"); int main(int argc, char** argv) { - std::vector files; + Files::PathContainer files, sources; bool writeDebugLog = false; - std::vector archives; - if (!parseOptions(argc, argv, files, writeDebugLog, archives)) + bool quiet = false; + if (!parseOptions(argc, argv, files, sources, writeDebugLog, quiet)) return 1; Nif::Reader::setLoadUnsupportedFiles(true); Nif::Reader::setWriteNifDebugLog(writeDebugLog); std::unique_ptr vfs; - if (!archives.empty()) + if (!sources.empty()) { vfs = std::make_unique(); - for (const std::filesystem::path& path : archives) + for (const std::filesystem::path& path : sources) { + const std::string pathStr = Files::pathToUnicodeString(path); + if (!quiet) + std::cout << "Adding data source '" << pathStr << "'" << std::endl; + try { if (auto archive = makeArchive(path)) vfs->addArchive(std::move(archive)); else - std::cerr << '"' << path << "\" is unsupported archive" << std::endl; - vfs->buildIndex(); + std::cerr << "Error: '" << pathStr << "' is not an archive or directory" << std::endl; } catch (std::exception& e) { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; + std::cerr << "Failed to add data source '" << pathStr << "': " << e.what() << std::endl; } } + + vfs->buildIndex(); } - // std::cout << "Reading Files" << std::endl; for (const auto& path : files) { + const std::string pathStr = Files::pathToUnicodeString(path); try { - if (isNIF(path)) + const bool isFile = readFile({}, path, vfs.get(), quiet); + if (!isFile) { - // std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile file(path); - Nif::Reader reader(file); - if (vfs != nullptr) - reader.parse(vfs->get(Files::pathToUnicodeString(path))); + if (auto archive = makeArchive(path)) + { + readVFS(std::move(archive), path, quiet); + } else - reader.parse(Files::openConstrainedFileStream(path)); - } - else if (auto archive = makeArchive(path)) - { - readVFS(std::move(archive), path); - } - else - { - std::cerr << "ERROR: \"" << Files::pathToUnicodeString(path) - << "\" is not a nif file, bsa file, or directory!" << std::endl; + { + std::cerr << "Error: '" << pathStr << "' is not a NIF file, material file, archive, or directory" + << std::endl; + } } } catch (std::exception& e) { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; + std::cerr << "Failed to read '" << pathStr << "': " << e.what() << std::endl; } } return 0; diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 20bd62d145..a131c56358 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -1,5 +1,4 @@ set (OPENCS_SRC - ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc ) opencs_units (. editor) @@ -9,7 +8,7 @@ opencs_units (model/doc ) opencs_units (model/doc - savingstate savingstages blacklist messages + savingstate savingstages messages ) opencs_hdrs (model/doc @@ -26,12 +25,12 @@ opencs_units (model/world opencs_units (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope - pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection + pathgrid land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection idcompletionmanager metadata defaultgmsts infoselectwrapper commandmacro ) opencs_hdrs (model/world - columnimp idcollection collection info subcellcollection + columnimp disabletag idcollection collection info subcellcollection ) @@ -43,7 +42,7 @@ opencs_units (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck startscriptcheck search searchoperation searchstage pathgridcheck soundgencheck magiceffectcheck - mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck + mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck effectlistcheck ) opencs_hdrs (model/tools @@ -71,7 +70,7 @@ opencs_units (view/world cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator - bodypartcreator landtexturecreator landcreator tableheadermouseeventhandler + bodypartcreator landcreator tableheadermouseeventhandler ) opencs_units (view/world @@ -116,7 +115,7 @@ opencs_units (view/prefs opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut - shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting + shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting subcategory ) opencs_units (model/prefs @@ -138,27 +137,18 @@ set (OPENCS_RES ${CMAKE_SOURCE_DIR}/files/opencs/resources.qrc ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc ) -set (OPENCS_UI - ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui - ${CMAKE_SOURCE_DIR}/files/ui/filedialog.ui - ) - source_group (openmw-cs FILES main.cpp ${OPENCS_SRC} ${OPENCS_HDR}) if(WIN32) + set(OPENCS_RES ${OPENCS_RES} ${CMAKE_SOURCE_DIR}/files/windows/QWindowsVistaDark/dark.qrc) set(QT_USE_QTMAIN TRUE) + set(OPENCS_RC_FILE ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc) +else(WIN32) + set(OPENCS_RC_FILE "") endif(WIN32) -if (QT_VERSION_MAJOR VERSION_EQUAL 5) - qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) -else () - qt6_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) -endif() qt_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) -# for compiled .ui files -include_directories(${CMAKE_CURRENT_BINARY_DIR}) - if(APPLE) set (OPENCS_MAC_ICON "${CMAKE_SOURCE_DIR}/files/mac/openmw-cs.icns") set (OPENCS_CFG "${OpenMW_BINARY_DIR}/defaults-cs.bin") @@ -171,15 +161,13 @@ else() set (OPENCS_OPENMW_CFG "") endif(APPLE) -add_library(openmw-cs-lib +add_library(openmw-cs-lib STATIC ${OPENCS_SRC} ${OPENCS_UI_HDR} ${OPENCS_MOC_SRC} ${OPENCS_RES_SRC} ) -set_target_properties(openmw-cs-lib PROPERTIES OUTPUT_NAME openmw-cs) - if(BUILD_OPENCS) openmw_add_executable(openmw-cs MACOSX_BUNDLE @@ -187,14 +175,19 @@ if(BUILD_OPENCS) ${OPENCS_CFG} ${OPENCS_DEFAULT_FILTERS_FILE} ${OPENCS_OPENMW_CFG} + ${OPENCS_RC_FILE} main.cpp ) target_link_libraries(openmw-cs openmw-cs-lib) + set_property(TARGET openmw-cs PROPERTY AUTOMOC ON) + set_property(TARGET openmw-cs PROPERTY AUTOUIC_SEARCH_PATHS ui) + set_property(TARGET openmw-cs PROPERTY AUTOUIC ON) + if (BUILD_WITH_CODE_COVERAGE) - target_compile_options(openmw-cs-lib PRIVATE --coverage) - target_link_libraries(openmw-cs-lib gcov) + target_compile_options(openmw-cs PRIVATE --coverage) + target_link_libraries(openmw-cs gcov) endif() endif() @@ -243,18 +236,18 @@ target_link_libraries(openmw-cs-lib ${OSGTEXT_LIBRARIES} ${OSG_LIBRARIES} ${EXTERN_OSGQT_LIBRARY} - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options components_qt ) if (QT_VERSION_MAJOR VERSION_EQUAL 6) - target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets) + target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) else() - target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL) + target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::Svg) endif() if (WIN32) - target_link_libraries(openmw-cs-lib ${Boost_LOCALE_LIBRARY}) + target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest) endif() if (WIN32 AND BUILD_OPENCS) @@ -283,6 +276,8 @@ endif() if(USE_QT) set_property(TARGET openmw-cs-lib PROPERTY AUTOMOC ON) + set_property(TARGET openmw-cs-lib PROPERTY AUTOUIC_SEARCH_PATHS ui) + set_property(TARGET openmw-cs-lib PROPERTY AUTOUIC ON) endif(USE_QT) if (BUILD_WITH_CODE_COVERAGE) @@ -290,7 +285,7 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(openmw-cs-lib gcov) endif() -if (MSVC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-cs-lib PRIVATE diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 416d059735..4cab88e5f2 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -21,7 +21,7 @@ #include #ifdef _WIN32 -#include +#include #endif #include @@ -119,8 +119,6 @@ boost::program_options::variables_map CS::Editor::readConfiguration() boost::program_options::value()->default_value( Files::MaybeQuotedPathContainer::value_type(), "")); addOption("encoding", boost::program_options::value()->default_value("win1252")); - addOption("resources", - boost::program_options::value()->default_value(Files::MaybeQuotedPath(), "resources")); addOption("fallback-archive", boost::program_options::value>() ->default_value(std::vector(), "fallback-archive") @@ -128,20 +126,13 @@ boost::program_options::variables_map CS::Editor::readConfiguration() addOption("fallback", boost::program_options::value()->default_value(FallbackMap(), "")->multitoken()->composing(), "fallback values"); - addOption("script-blacklist", - boost::program_options::value>() - ->default_value(std::vector(), "") - ->multitoken(), - "exclude specified script from the verifier (if the use of the blacklist is enabled)"); - addOption("script-blacklist-use", boost::program_options::value()->implicit_value(true)->default_value(true), - "enable script blacklisting"); Files::ConfigurationManager::addCommonOptions(desc); boost::program_options::notify(variables); mCfgMgr.readConfiguration(variables, desc, false); Settings::Manager::load(mCfgMgr, true); - setupLogging(mCfgMgr.getLogPath(), "OpenMW-CS"); + Debug::setupLogging(mCfgMgr.getLogPath(), "OpenMW-CS"); return variables; } @@ -161,9 +152,6 @@ std::pair> CS::Editor::readConfig .u8string()); // This call to u8string is redundant, but required // to build on MSVC 14.26 due to implementation bugs. - if (variables["script-blacklist-use"].as()) - mDocumentManager.setBlacklistedScripts(variables["script-blacklist"].as>()); - Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { @@ -200,6 +188,8 @@ std::pair> CS::Editor::readConfig dataDirs.insert(dataDirs.end(), dataLocal.begin(), dataLocal.end()); + dataDirs.insert(dataDirs.begin(), mResources / "vfs"); + // iterate the data directories and add them to the file dialog for loading mFileDialog.addFiles(dataDirs); diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index ecab9614a1..f2e2156865 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -3,17 +3,17 @@ #include #include -#include #include #include #include #include -#include +#include #include #include "model/doc/messages.hpp" +#include "model/world/disabletag.hpp" #include "model/world/universalid.hpp" #ifdef Q_OS_MAC @@ -25,30 +25,6 @@ Q_DECLARE_METATYPE(std::string) class QEvent; class QObject; -class Application : public QApplication -{ -private: - bool notify(QObject* receiver, QEvent* event) override - { - try - { - return QApplication::notify(receiver, event); - } - catch (const std::exception& exception) - { - Log(Debug::Error) << "An exception has been caught: " << exception.what(); - } - - return false; - } - -public: - Application(int& argc, char* argv[]) - : QApplication(argc, argv) - { - } -}; - void setQSurfaceFormat() { osg::DisplaySettings* ds = osg::DisplaySettings::instance().get(); @@ -72,21 +48,21 @@ int runApplication(int argc, char* argv[]) Q_INIT_RESOURCE(resources); +#ifdef WIN32 + Q_INIT_RESOURCE(dark); +#endif + qRegisterMetaType("std::string"); qRegisterMetaType("CSMWorld::UniversalId"); + qRegisterMetaType("CSMWorld::DisableTag"); qRegisterMetaType("CSMDoc::Message"); setQSurfaceFormat(); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - Application application(argc, argv); + Platform::Application application(argc, argv); -#ifdef Q_OS_MAC - QDir dir(QCoreApplication::applicationDirPath()); - QDir::setCurrent(dir.absolutePath()); -#endif - - application.setWindowIcon(QIcon(":./openmw-cs.png")); + application.setWindowIcon(QIcon(":openmw-cs")); CS::Editor editor(argc, argv); #ifdef __linux__ @@ -104,5 +80,5 @@ int runApplication(int argc, char* argv[]) int main(int argc, char* argv[]) { - return wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); + return Debug::wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); } diff --git a/apps/opencs/model/doc/blacklist.cpp b/apps/opencs/model/doc/blacklist.cpp deleted file mode 100644 index 9b422cb751..0000000000 --- a/apps/opencs/model/doc/blacklist.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "blacklist.hpp" - -#include -#include -#include - -#include - -#include - -bool CSMDoc::Blacklist::isBlacklisted(const CSMWorld::UniversalId& id) const -{ - std::map>::const_iterator iter = mIds.find(id.getType()); - - if (iter == mIds.end()) - return false; - - return std::binary_search(iter->second.begin(), iter->second.end(), Misc::StringUtils::lowerCase(id.getId())); -} - -void CSMDoc::Blacklist::add(CSMWorld::UniversalId::Type type, const std::vector& ids) -{ - std::vector& list = mIds[type]; - - size_t size = list.size(); - - list.resize(size + ids.size()); - - std::transform(ids.begin(), ids.end(), list.begin() + size, - [](const std::string& s) { return Misc::StringUtils::lowerCase(s); }); - std::sort(list.begin(), list.end()); -} diff --git a/apps/opencs/model/doc/blacklist.hpp b/apps/opencs/model/doc/blacklist.hpp deleted file mode 100644 index e565aa252b..0000000000 --- a/apps/opencs/model/doc/blacklist.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef CSM_DOC_BLACKLIST_H -#define CSM_DOC_BLACKLIST_H - -#include -#include -#include - -#include "../world/universalid.hpp" - -namespace CSMDoc -{ - /// \brief ID blacklist sorted by UniversalId type - class Blacklist - { - std::map> mIds; - - public: - bool isBlacklisted(const CSMWorld::UniversalId& id) const; - - void add(CSMWorld::UniversalId::Type type, const std::vector& ids); - }; -} - -#endif diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index f604608c7b..21e5ab306b 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -2,7 +2,6 @@ #include "state.hpp" -#include #include #include #include @@ -295,8 +294,7 @@ void CSMDoc::Document::createBase() CSMDoc::Document::Document(const Files::ConfigurationManager& configuration, std::vector files, bool new_, const std::filesystem::path& savePath, const std::filesystem::path& resDir, ToUTF8::FromType encoding, - const std::vector& blacklistedScripts, const Files::PathContainer& dataPaths, - const std::vector& archives) + const Files::PathContainer& dataPaths, const std::vector& archives) : mSavePath(savePath) , mContentFiles(std::move(files)) , mNew(new_) @@ -339,8 +337,6 @@ CSMDoc::Document::Document(const Files::ConfigurationManager& configuration, std createBase(); } - mBlacklist.add(CSMWorld::UniversalId::Type_Script, blacklistedScripts); - addOptionalGmsts(); addOptionalGlobals(); addOptionalMagicEffects(); @@ -485,11 +481,6 @@ CSMTools::ReportModel* CSMDoc::Document::getReport(const CSMWorld::UniversalId& return mTools.getReport(id); } -bool CSMDoc::Document::isBlacklisted(const CSMWorld::UniversalId& id) const -{ - return mBlacklist.isBlacklisted(id); -} - void CSMDoc::Document::startRunning(const std::string& profile, const std::string& startupInstruction) { std::vector contentFiles; diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index 4acdfafa41..60516cdc8c 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -19,7 +19,6 @@ #include "../tools/tools.hpp" -#include "blacklist.hpp" #include "operationholder.hpp" #include "runner.hpp" #include "saving.hpp" @@ -61,7 +60,6 @@ namespace CSMDoc Saving mSavingOperation; OperationHolder mSaving; std::filesystem::path mResDir; - Blacklist mBlacklist; Runner mRunner; bool mDirty; @@ -95,8 +93,7 @@ namespace CSMDoc public: Document(const Files::ConfigurationManager& configuration, std::vector files, bool new_, const std::filesystem::path& savePath, const std::filesystem::path& resDir, ToUTF8::FromType encoding, - const std::vector& blacklistedScripts, const Files::PathContainer& dataPaths, - const std::vector& archives); + const Files::PathContainer& dataPaths, const std::vector& archives); ~Document() override = default; @@ -136,8 +133,6 @@ namespace CSMDoc CSMTools::ReportModel* getReport(const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. - bool isBlacklisted(const CSMWorld::UniversalId& id) const; - void startRunning(const std::string& profile, const std::string& startupInstruction = ""); void stopRunning(); diff --git a/apps/opencs/model/doc/documentmanager.cpp b/apps/opencs/model/doc/documentmanager.cpp index 4052c8a789..72c315b004 100644 --- a/apps/opencs/model/doc/documentmanager.cpp +++ b/apps/opencs/model/doc/documentmanager.cpp @@ -61,8 +61,7 @@ void CSMDoc::DocumentManager::addDocument( CSMDoc::Document* CSMDoc::DocumentManager::makeDocument( const std::vector& files, const std::filesystem::path& savePath, bool new_) { - return new Document( - mConfiguration, files, new_, savePath, mResDir, mEncoding, mBlacklistedScripts, mDataPaths, mArchives); + return new Document(mConfiguration, files, new_, savePath, mResDir, mEncoding, mDataPaths, mArchives); } void CSMDoc::DocumentManager::insertDocument(CSMDoc::Document* document) @@ -102,11 +101,6 @@ void CSMDoc::DocumentManager::setEncoding(ToUTF8::FromType encoding) mEncoding = encoding; } -void CSMDoc::DocumentManager::setBlacklistedScripts(const std::vector& scriptIds) -{ - mBlacklistedScripts = scriptIds; -} - void CSMDoc::DocumentManager::documentLoaded(Document* document) { emit documentAdded(document); diff --git a/apps/opencs/model/doc/documentmanager.hpp b/apps/opencs/model/doc/documentmanager.hpp index 25f7d1d4f0..2c9ee1e98e 100644 --- a/apps/opencs/model/doc/documentmanager.hpp +++ b/apps/opencs/model/doc/documentmanager.hpp @@ -31,7 +31,6 @@ namespace CSMDoc QThread mLoaderThread; Loader mLoader; ToUTF8::FromType mEncoding; - std::vector mBlacklistedScripts; std::filesystem::path mResDir; @@ -64,8 +63,6 @@ namespace CSMDoc void setEncoding(ToUTF8::FromType encoding); - void setBlacklistedScripts(const std::vector& scriptIds); - /// Sets the file data that gets passed to newly created documents. void setFileData(const Files::PathContainer& dataPaths, const std::vector& archives); diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index 46dea447fe..5be733e0d1 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -17,13 +17,6 @@ #include "document.hpp" -CSMDoc::Loader::Stage::Stage() - : mFile(0) - , mRecordsLoaded(0) - , mRecordsLeft(false) -{ -} - CSMDoc::Loader::Loader() : mShouldStop(false) { @@ -105,7 +98,7 @@ void CSMDoc::Loader::load() if (iter->second.mFile < size) // start loading the files { - std::filesystem::path path = document->getContentFiles()[iter->second.mFile]; + const std::filesystem::path& path = document->getContentFiles()[iter->second.mFile]; int steps = document->getData().startLoading(path, iter->second.mFile != editedIndex, /*project*/ false); iter->second.mRecordsLeft = true; diff --git a/apps/opencs/model/doc/loader.hpp b/apps/opencs/model/doc/loader.hpp index ccf493d19e..79b4524f4f 100644 --- a/apps/opencs/model/doc/loader.hpp +++ b/apps/opencs/model/doc/loader.hpp @@ -23,11 +23,9 @@ namespace CSMDoc struct Stage { - int mFile; - int mRecordsLoaded; - bool mRecordsLeft; - - Stage(); + int mFile = 0; + int mRecordsLoaded = 0; + bool mRecordsLeft = false; }; QMutex mMutex; diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp index 0099cb2f94..d647d6b498 100644 --- a/apps/opencs/model/doc/runner.cpp +++ b/apps/opencs/model/doc/runner.cpp @@ -93,7 +93,6 @@ void CSMDoc::Runner::start(bool delayed) arguments << "--data=\"" + Files::pathToQString(mProjectPath.parent_path()) + "\""; arguments << "--replace=content"; - arguments << "--content=builtin.omwscripts"; for (const auto& mContentFile : mContentFiles) { diff --git a/apps/opencs/model/doc/saving.cpp b/apps/opencs/model/doc/saving.cpp index b2e4d4649a..868429f96c 100644 --- a/apps/opencs/model/doc/saving.cpp +++ b/apps/opencs/model/doc/saving.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "../world/data.hpp" #include "../world/idcollection.hpp" @@ -52,6 +53,9 @@ CSMDoc::Saving::Saving(Document& document, const std::filesystem::path& projectP appendStage(new WriteCollectionStage>( mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getSelectionGroups(), mState, CSMWorld::Scope_Project)); + appendStage(new CloseSaveStage(mState)); // save content file @@ -93,9 +97,6 @@ CSMDoc::Saving::Saving(Document& document, const std::filesystem::path& projectP appendStage( new WriteCollectionStage>(mDocument.getData().getBodyParts(), mState)); - appendStage(new WriteCollectionStage>( - mDocument.getData().getSoundGens(), mState)); - appendStage(new WriteCollectionStage>( mDocument.getData().getMagicEffects(), mState)); @@ -104,16 +105,21 @@ CSMDoc::Saving::Saving(Document& document, const std::filesystem::path& projectP appendStage(new WriteRefIdCollectionStage(mDocument, mState)); + // Can reference creatures so needs to load after them for TESCS compatibility + appendStage(new WriteCollectionStage>( + mDocument.getData().getSoundGens(), mState)); + appendStage(new CollectionReferencesStage(mDocument, mState)); appendStage(new WriteCellCollectionStage(mDocument, mState)); - // Dialogue can reference objects and cells so must be written after these records for vanilla-compatible files - - appendStage(new WriteDialogueCollectionStage(mDocument, mState, false)); + // Dialogue can reference objects, cells, and journals so must be written after these records for vanilla-compatible + // files appendStage(new WriteDialogueCollectionStage(mDocument, mState, true)); + appendStage(new WriteDialogueCollectionStage(mDocument, mState, false)); + appendStage(new WritePathgridCollectionStage(mDocument, mState)); appendStage(new WriteLandTextureCollectionStage(mDocument, mState)); diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 82135e0042..12fa6e811f 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -135,7 +134,7 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages if (topic.mState == CSMWorld::RecordBase::State_Deleted) { // if the topic is deleted, we do not need to bother with INFO records. - ESM::Dialogue dialogue = topic.get(); + const ESM::Dialogue& dialogue = topic.get(); writer.startRecord(dialogue.sRecordId); dialogue.save(writer, true); writer.endRecord(dialogue.sRecordId); @@ -187,6 +186,7 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages { ESM::DialInfo info = record.get(); info.mId = record.get().mOriginalId; + info.mData.mType = topic.get().mType; if (iter == infos.begin()) info.mPrev = ESM::RefId(); @@ -304,9 +304,8 @@ void CSMDoc::WriteCellCollectionStage::writeReferences( { CSMWorld::CellRef refRecord = ref.get(); - // Check for uninitialized content file - if (!refRecord.mRefNum.hasContentFile()) - refRecord.mRefNum.mContentFile = 0; + // -1 is the current file, saved indices are 1-based + refRecord.mRefNum.mContentFile++; // recalculate the ref's cell location std::ostringstream stream; @@ -498,11 +497,11 @@ int CSMDoc::WriteLandTextureCollectionStage::setup() void CSMDoc::WriteLandTextureCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); - const CSMWorld::Record& landTexture = mDocument.getData().getLandTextures().getRecord(stage); + const CSMWorld::Record& landTexture = mDocument.getData().getLandTextures().getRecord(stage); if (landTexture.isModified() || landTexture.mState == CSMWorld::RecordBase::State_Deleted) { - CSMWorld::LandTexture record = landTexture.get(); + ESM::LandTexture record = landTexture.get(); writer.startRecord(record.sRecordId); record.save(writer, landTexture.mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord(record.sRecordId); diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 5443db2854..6248b03bb4 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -452,7 +452,10 @@ std::shared_ptr CSMFilter::Parser::parseText() return std::shared_ptr(); } - return std::make_shared(columnId, text); + auto node = std::make_shared(columnId, text); + if (!node->isValid()) + error(); + return node; } std::shared_ptr CSMFilter::Parser::parseValue() @@ -624,7 +627,7 @@ bool CSMFilter::Parser::parse(const std::string& filter, bool allowPredefined) } if (node) - mFilter = node; + mFilter = std::move(node); else { // Empty filter string equals to filter "true". diff --git a/apps/opencs/model/filter/textnode.hpp b/apps/opencs/model/filter/textnode.hpp index 14efa0a3a0..d629cbe336 100644 --- a/apps/opencs/model/filter/textnode.hpp +++ b/apps/opencs/model/filter/textnode.hpp @@ -34,6 +34,8 @@ namespace CSMFilter ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. + + bool isValid() { return mRegExp.isValid(); } }; } diff --git a/apps/opencs/model/prefs/boolsetting.cpp b/apps/opencs/model/prefs/boolsetting.cpp index c668bc0af4..44262e2012 100644 --- a/apps/opencs/model/prefs/boolsetting.cpp +++ b/apps/opencs/model/prefs/boolsetting.cpp @@ -11,9 +11,8 @@ #include "state.hpp" CSMPrefs::BoolSetting::BoolSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, bool default_) - : Setting(parent, mutex, key, label) - , mDefault(default_) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mWidget(nullptr) { } @@ -24,10 +23,10 @@ CSMPrefs::BoolSetting& CSMPrefs::BoolSetting::setTooltip(const std::string& tool return *this; } -std::pair CSMPrefs::BoolSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::BoolSetting::makeWidgets(QWidget* parent) { - mWidget = new QCheckBox(QString::fromUtf8(getLabel().c_str()), parent); - mWidget->setCheckState(mDefault ? Qt::Checked : Qt::Unchecked); + mWidget = new QCheckBox(getLabel(), parent); + mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); if (!mTooltip.empty()) { @@ -37,24 +36,19 @@ std::pair CSMPrefs::BoolSetting::makeWidgets(QWidget* parent connect(mWidget, &QCheckBox::stateChanged, this, &BoolSetting::valueChanged); - return std::make_pair(static_cast(nullptr), mWidget); + return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; } void CSMPrefs::BoolSetting::updateWidget() { if (mWidget) { - mWidget->setCheckState( - Settings::Manager::getBool(getKey(), getParent()->getKey()) ? Qt::Checked : Qt::Unchecked); + mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); } } void CSMPrefs::BoolSetting::valueChanged(int value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setBool(getKey(), getParent()->getKey(), value); - } - + setValue(value != Qt::Unchecked); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/boolsetting.hpp b/apps/opencs/model/prefs/boolsetting.hpp index e75ea1a346..edabf85058 100644 --- a/apps/opencs/model/prefs/boolsetting.hpp +++ b/apps/opencs/model/prefs/boolsetting.hpp @@ -12,21 +12,21 @@ namespace CSMPrefs { class Category; - class BoolSetting : public Setting + class BoolSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - bool mDefault; QCheckBox* mWidget; public: - BoolSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, bool default_); + explicit BoolSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); BoolSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/category.cpp b/apps/opencs/model/prefs/category.cpp index 5a82be08fc..3ae4826953 100644 --- a/apps/opencs/model/prefs/category.cpp +++ b/apps/opencs/model/prefs/category.cpp @@ -5,6 +5,7 @@ #include "setting.hpp" #include "state.hpp" +#include "subcategory.hpp" CSMPrefs::Category::Category(State* parent, const std::string& key) : mParent(parent) @@ -23,6 +24,14 @@ CSMPrefs::State* CSMPrefs::Category::getState() const } void CSMPrefs::Category::addSetting(Setting* setting) +{ + if (!mIndex.emplace(setting->getKey(), setting).second) + throw std::logic_error("Category " + mKey + " already has setting: " + setting->getKey()); + + mSettings.push_back(setting); +} + +void CSMPrefs::Category::addSubcategory(Subcategory* setting) { mSettings.push_back(setting); } @@ -39,11 +48,12 @@ CSMPrefs::Category::Iterator CSMPrefs::Category::end() CSMPrefs::Setting& CSMPrefs::Category::operator[](const std::string& key) { - for (Iterator iter = mSettings.begin(); iter != mSettings.end(); ++iter) - if ((*iter)->getKey() == key) - return **iter; + const auto it = mIndex.find(key); - throw std::logic_error("Invalid user setting: " + key); + if (it != mIndex.end()) + return *it->second; + + throw std::logic_error("Invalid user setting in " + mKey + " category: " + key); } void CSMPrefs::Category::update() diff --git a/apps/opencs/model/prefs/category.hpp b/apps/opencs/model/prefs/category.hpp index 5c75f99067..ef67c82138 100644 --- a/apps/opencs/model/prefs/category.hpp +++ b/apps/opencs/model/prefs/category.hpp @@ -3,12 +3,14 @@ #include #include +#include #include namespace CSMPrefs { class State; class Setting; + class Subcategory; class Category { @@ -20,6 +22,7 @@ namespace CSMPrefs State* mParent; std::string mKey; Container mSettings; + std::unordered_map mIndex; public: Category(State* parent, const std::string& key); @@ -30,6 +33,8 @@ namespace CSMPrefs void addSetting(Setting* setting); + void addSubcategory(Subcategory* setting); + Iterator begin(); Iterator end(); diff --git a/apps/opencs/model/prefs/coloursetting.cpp b/apps/opencs/model/prefs/coloursetting.cpp index 86f3a5d772..10ca9d7f68 100644 --- a/apps/opencs/model/prefs/coloursetting.cpp +++ b/apps/opencs/model/prefs/coloursetting.cpp @@ -14,9 +14,8 @@ #include "state.hpp" CSMPrefs::ColourSetting::ColourSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, QColor default_) - : Setting(parent, mutex, key, label) - , mDefault(std::move(default_)) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mWidget(nullptr) { } @@ -27,11 +26,11 @@ CSMPrefs::ColourSetting& CSMPrefs::ColourSetting::setTooltip(const std::string& return *this; } -std::pair CSMPrefs::ColourSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::ColourSetting::makeWidgets(QWidget* parent) { - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); - mWidget = new CSVWidget::ColorEditor(mDefault, parent); + mWidget = new CSVWidget::ColorEditor(toColor(), parent); if (!mTooltip.empty()) { @@ -42,24 +41,18 @@ std::pair CSMPrefs::ColourSetting::makeWidgets(QWidget* pare connect(mWidget, &CSVWidget::ColorEditor::pickingFinished, this, &ColourSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::ColourSetting::updateWidget() { if (mWidget) - { - mWidget->setColor(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); - } + mWidget->setColor(toColor()); } void CSMPrefs::ColourSetting::valueChanged() { CSVWidget::ColorEditor& widget = dynamic_cast(*sender()); - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), widget.color().name().toUtf8().data()); - } - + setValue(widget.color().name().toStdString()); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/coloursetting.hpp b/apps/opencs/model/prefs/coloursetting.hpp index 0c22d9cc5d..85e43f28bd 100644 --- a/apps/opencs/model/prefs/coloursetting.hpp +++ b/apps/opencs/model/prefs/coloursetting.hpp @@ -6,6 +6,7 @@ #include #include +#include #include class QMutex; @@ -20,22 +21,22 @@ namespace CSVWidget namespace CSMPrefs { class Category; - class ColourSetting : public Setting + + class ColourSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - QColor mDefault; CSVWidget::ColorEditor* mWidget; public: - ColourSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, QColor default_); + explicit ColourSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); ColourSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/doublesetting.cpp b/apps/opencs/model/prefs/doublesetting.cpp index 7e3aadb0c3..bbe573f800 100644 --- a/apps/opencs/model/prefs/doublesetting.cpp +++ b/apps/opencs/model/prefs/doublesetting.cpp @@ -15,12 +15,11 @@ #include "state.hpp" CSMPrefs::DoubleSetting::DoubleSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, double default_) - : Setting(parent, mutex, key, label) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mPrecision(2) , mMin(0) , mMax(std::numeric_limits::max()) - , mDefault(default_) , mWidget(nullptr) { } @@ -56,14 +55,14 @@ CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setTooltip(const std::string& return *this; } -std::pair CSMPrefs::DoubleSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::DoubleSetting::makeWidgets(QWidget* parent) { - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); mWidget = new QDoubleSpinBox(parent); mWidget->setDecimals(mPrecision); mWidget->setRange(mMin, mMax); - mWidget->setValue(mDefault); + mWidget->setValue(getValue()); if (!mTooltip.empty()) { @@ -74,23 +73,17 @@ std::pair CSMPrefs::DoubleSetting::makeWidgets(QWidget* pare connect(mWidget, qOverload(&QDoubleSpinBox::valueChanged), this, &DoubleSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::DoubleSetting::updateWidget() { if (mWidget) - { - mWidget->setValue(Settings::Manager::getFloat(getKey(), getParent()->getKey())); - } + mWidget->setValue(getValue()); } void CSMPrefs::DoubleSetting::valueChanged(double value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setFloat(getKey(), getParent()->getKey(), value); - } - + setValue(value); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/doublesetting.hpp b/apps/opencs/model/prefs/doublesetting.hpp index c951d2a88c..856cebcb46 100644 --- a/apps/opencs/model/prefs/doublesetting.hpp +++ b/apps/opencs/model/prefs/doublesetting.hpp @@ -9,7 +9,7 @@ namespace CSMPrefs { class Category; - class DoubleSetting : public Setting + class DoubleSetting final : public TypedSetting { Q_OBJECT @@ -17,12 +17,11 @@ namespace CSMPrefs double mMin; double mMax; std::string mTooltip; - double mDefault; QDoubleSpinBox* mWidget; public: - DoubleSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, double default_); + explicit DoubleSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); DoubleSetting& setPrecision(int precision); @@ -36,7 +35,7 @@ namespace CSMPrefs DoubleSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/enumsetting.cpp b/apps/opencs/model/prefs/enumsetting.cpp index a3ac9bce2b..aaa4c28c61 100644 --- a/apps/opencs/model/prefs/enumsetting.cpp +++ b/apps/opencs/model/prefs/enumsetting.cpp @@ -15,39 +15,10 @@ #include "category.hpp" #include "state.hpp" -CSMPrefs::EnumValue::EnumValue(const std::string& value, const std::string& tooltip) - : mValue(value) - , mTooltip(tooltip) -{ -} - -CSMPrefs::EnumValue::EnumValue(const char* value) - : mValue(value) -{ -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add(const EnumValues& values) -{ - mValues.insert(mValues.end(), values.mValues.begin(), values.mValues.end()); - return *this; -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add(const EnumValue& value) -{ - mValues.push_back(value); - return *this; -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add(const std::string& value, const std::string& tooltip) -{ - mValues.emplace_back(value, tooltip); - return *this; -} - -CSMPrefs::EnumSetting::EnumSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, const EnumValue& default_) - : Setting(parent, mutex, key, label) - , mDefault(default_) +CSMPrefs::EnumSetting::EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, + std::span values, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mValues(values) , mWidget(nullptr) { } @@ -58,43 +29,28 @@ CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip(const std::string& tool return *this; } -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValues(const EnumValues& values) +CSMPrefs::SettingWidgets CSMPrefs::EnumSetting::makeWidgets(QWidget* parent) { - mValues.add(values); - return *this; -} - -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue(const EnumValue& value) -{ - mValues.add(value); - return *this; -} - -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue(const std::string& value, const std::string& tooltip) -{ - mValues.add(value, tooltip); - return *this; -} - -std::pair CSMPrefs::EnumSetting::makeWidgets(QWidget* parent) -{ - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); mWidget = new QComboBox(parent); - size_t index = 0; - - for (size_t i = 0; i < mValues.mValues.size(); ++i) + for (std::size_t i = 0; i < mValues.size(); ++i) { - if (mDefault.mValue == mValues.mValues[i].mValue) - index = i; + const EnumValueView& v = mValues[i]; - mWidget->addItem(QString::fromUtf8(mValues.mValues[i].mValue.c_str())); + mWidget->addItem(QString::fromUtf8(v.mValue.data(), static_cast(v.mValue.size()))); - if (!mValues.mValues[i].mTooltip.empty()) - mWidget->setItemData(i, QString::fromUtf8(mValues.mValues[i].mTooltip.c_str()), Qt::ToolTipRole); + if (!v.mTooltip.empty()) + mWidget->setItemData(static_cast(i), + QString::fromUtf8(v.mTooltip.data(), static_cast(v.mTooltip.size())), Qt::ToolTipRole); } + const std::string value = getValue(); + const std::size_t index = std::find_if(mValues.begin(), mValues.end(), [&](const EnumValueView& v) { + return v.mValue == value; + }) - mValues.begin(); + mWidget->setCurrentIndex(static_cast(index)); if (!mTooltip.empty()) @@ -105,26 +61,20 @@ std::pair CSMPrefs::EnumSetting::makeWidgets(QWidget* parent connect(mWidget, qOverload(&QComboBox::currentIndexChanged), this, &EnumSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::EnumSetting::updateWidget() { if (mWidget) - { - int index - = mWidget->findText(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); - - mWidget->setCurrentIndex(index); - } + mWidget->setCurrentIndex(mWidget->findText(QString::fromStdString(getValue()))); } void CSMPrefs::EnumSetting::valueChanged(int value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), mValues.mValues.at(value).mValue); - } + if (value < 0 || static_cast(value) >= mValues.size()) + throw std::logic_error("Invalid enum setting \"" + getKey() + "\" value index: " + std::to_string(value)); + setValue(std::string(mValues[value].mValue)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/enumsetting.hpp b/apps/opencs/model/prefs/enumsetting.hpp index 57bd2115ce..00953f914e 100644 --- a/apps/opencs/model/prefs/enumsetting.hpp +++ b/apps/opencs/model/prefs/enumsetting.hpp @@ -1,10 +1,13 @@ #ifndef CSM_PREFS_ENUMSETTING_H #define CSM_PREFS_ENUMSETTING_H +#include #include +#include #include #include +#include "enumvalueview.hpp" #include "setting.hpp" class QComboBox; @@ -13,50 +16,22 @@ namespace CSMPrefs { class Category; - struct EnumValue - { - std::string mValue; - std::string mTooltip; - - EnumValue(const std::string& value, const std::string& tooltip = ""); - - EnumValue(const char* value); - }; - - struct EnumValues - { - std::vector mValues; - - EnumValues& add(const EnumValues& values); - - EnumValues& add(const EnumValue& value); - - EnumValues& add(const std::string& value, const std::string& tooltip); - }; - - class EnumSetting : public Setting + class EnumSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - EnumValue mDefault; - EnumValues mValues; + std::span mValues; QComboBox* mWidget; public: - EnumSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, - const EnumValue& default_); + explicit EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, + std::span values, Settings::Index& index); EnumSetting& setTooltip(const std::string& tooltip); - EnumSetting& addValues(const EnumValues& values); - - EnumSetting& addValue(const EnumValue& value); - - EnumSetting& addValue(const std::string& value, const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/enumvalueview.hpp b/apps/opencs/model/prefs/enumvalueview.hpp new file mode 100644 index 0000000000..f46d250a81 --- /dev/null +++ b/apps/opencs/model/prefs/enumvalueview.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_ENUMVALUEVIEW_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_ENUMVALUEVIEW_H + +#include + +namespace CSMPrefs +{ + struct EnumValueView + { + std::string_view mValue; + std::string_view mTooltip; + }; +} + +#endif diff --git a/apps/opencs/model/prefs/intsetting.cpp b/apps/opencs/model/prefs/intsetting.cpp index 90cc77c788..a593b6f688 100644 --- a/apps/opencs/model/prefs/intsetting.cpp +++ b/apps/opencs/model/prefs/intsetting.cpp @@ -15,11 +15,10 @@ #include "state.hpp" CSMPrefs::IntSetting::IntSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, int default_) - : Setting(parent, mutex, key, label) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mMin(0) , mMax(std::numeric_limits::max()) - , mDefault(default_) , mWidget(nullptr) { } @@ -49,13 +48,13 @@ CSMPrefs::IntSetting& CSMPrefs::IntSetting::setTooltip(const std::string& toolti return *this; } -std::pair CSMPrefs::IntSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::IntSetting::makeWidgets(QWidget* parent) { - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); mWidget = new QSpinBox(parent); mWidget->setRange(mMin, mMax); - mWidget->setValue(mDefault); + mWidget->setValue(getValue()); if (!mTooltip.empty()) { @@ -66,23 +65,17 @@ std::pair CSMPrefs::IntSetting::makeWidgets(QWidget* parent) connect(mWidget, qOverload(&QSpinBox::valueChanged), this, &IntSetting::valueChanged); - return std::make_pair(label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::IntSetting::updateWidget() { if (mWidget) - { - mWidget->setValue(Settings::Manager::getInt(getKey(), getParent()->getKey())); - } + mWidget->setValue(getValue()); } void CSMPrefs::IntSetting::valueChanged(int value) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setInt(getKey(), getParent()->getKey(), value); - } - + setValue(value); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/intsetting.hpp b/apps/opencs/model/prefs/intsetting.hpp index 8a655178a4..e2926456aa 100644 --- a/apps/opencs/model/prefs/intsetting.hpp +++ b/apps/opencs/model/prefs/intsetting.hpp @@ -12,18 +12,18 @@ namespace CSMPrefs { class Category; - class IntSetting : public Setting + class IntSetting final : public TypedSetting { Q_OBJECT int mMin; int mMax; std::string mTooltip; - int mDefault; QSpinBox* mWidget; public: - IntSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, int default_); + explicit IntSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); // defaults to [0, std::numeric_limits::max()] IntSetting& setRange(int min, int max); @@ -35,7 +35,7 @@ namespace CSMPrefs IntSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/modifiersetting.cpp b/apps/opencs/model/prefs/modifiersetting.cpp index 8752a4d51e..4bb7d64e60 100644 --- a/apps/opencs/model/prefs/modifiersetting.cpp +++ b/apps/opencs/model/prefs/modifiersetting.cpp @@ -19,21 +19,22 @@ class QWidget; namespace CSMPrefs { - ModifierSetting::ModifierSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) - : Setting(parent, mutex, key, label) + ModifierSetting::ModifierSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mButton(nullptr) , mEditorActive(false) { } - std::pair ModifierSetting::makeWidgets(QWidget* parent) + SettingWidgets ModifierSetting::makeWidgets(QWidget* parent) { int modifier = 0; State::get().getShortcutManager().getModifier(getKey(), modifier); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(modifier).c_str()); - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); @@ -46,14 +47,14 @@ namespace CSMPrefs connect(widget, &QPushButton::toggled, this, &ModifierSetting::buttonToggled); - return std::make_pair(label, widget); + return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ModifierSetting::updateWidget() { if (mButton) { - const std::string& shortcut = Settings::Manager::getString(getKey(), getParent()->getKey()); + const std::string& shortcut = getValue(); int modifier; State::get().getShortcutManager().convertFromString(shortcut, modifier); @@ -131,15 +132,7 @@ namespace CSMPrefs void ModifierSetting::storeValue(int modifier) { State::get().getShortcutManager().setModifier(getKey(), modifier); - - // Convert to string and assign - std::string value = State::get().getShortcutManager().convertToString(modifier); - - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), value); - } - + setValue(State::get().getShortcutManager().convertToString(modifier)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/modifiersetting.hpp b/apps/opencs/model/prefs/modifiersetting.hpp index ae984243ac..76a3a82e71 100644 --- a/apps/opencs/model/prefs/modifiersetting.hpp +++ b/apps/opencs/model/prefs/modifiersetting.hpp @@ -15,14 +15,16 @@ class QPushButton; namespace CSMPrefs { class Category; - class ModifierSetting : public Setting + + class ModifierSetting final : public TypedSetting { Q_OBJECT public: - ModifierSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); + explicit ModifierSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/setting.cpp b/apps/opencs/model/prefs/setting.cpp index efe360d1e8..3c2ac65c94 100644 --- a/apps/opencs/model/prefs/setting.cpp +++ b/apps/opencs/model/prefs/setting.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "category.hpp" #include "state.hpp" @@ -14,22 +15,17 @@ QMutex* CSMPrefs::Setting::getMutex() return mMutex; } -CSMPrefs::Setting::Setting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) +CSMPrefs::Setting::Setting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) : QObject(parent->getState()) , mParent(parent) , mMutex(mutex) , mKey(key) , mLabel(label) + , mIndex(index) { } -std::pair CSMPrefs::Setting::makeWidgets(QWidget* parent) -{ - return std::pair(0, 0); -} - -void CSMPrefs::Setting::updateWidget() {} - const CSMPrefs::Category* CSMPrefs::Setting::getParent() const { return mParent; @@ -40,35 +36,6 @@ const std::string& CSMPrefs::Setting::getKey() const return mKey; } -const std::string& CSMPrefs::Setting::getLabel() const -{ - return mLabel; -} - -int CSMPrefs::Setting::toInt() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getInt(mKey, mParent->getKey()); -} - -double CSMPrefs::Setting::toDouble() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getFloat(mKey, mParent->getKey()); -} - -std::string CSMPrefs::Setting::toString() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getString(mKey, mParent->getKey()); -} - -bool CSMPrefs::Setting::isTrue() const -{ - QMutexLocker lock(mMutex); - return Settings::Manager::getBool(mKey, mParent->getKey()); -} - QColor CSMPrefs::Setting::toColor() const { // toString() handles lock diff --git a/apps/opencs/model/prefs/setting.hpp b/apps/opencs/model/prefs/setting.hpp index f63271b3f2..faadbcadd1 100644 --- a/apps/opencs/model/prefs/setting.hpp +++ b/apps/opencs/model/prefs/setting.hpp @@ -4,15 +4,26 @@ #include #include +#include #include +#include + +#include "category.hpp" + class QWidget; class QColor; class QMutex; +class QGridLayout; +class QLabel; namespace CSMPrefs { - class Category; + struct SettingWidgets + { + QLabel* mLabel; + QWidget* mInput; + }; class Setting : public QObject { @@ -21,44 +32,82 @@ namespace CSMPrefs Category* mParent; QMutex* mMutex; std::string mKey; - std::string mLabel; + QString mLabel; + Settings::Index& mIndex; protected: QMutex* getMutex(); + template + void resetValueImpl() + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).reset(); + } + + template + T getValueImpl() const + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).get(); + } + + template + void setValueImpl(const T& value) + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).set(value); + } + public: - Setting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); + explicit Setting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); ~Setting() override = default; - /// Return label, input widget. - /// - /// \note first can be a 0-pointer, which means that the label is part of the input - /// widget. - virtual std::pair makeWidgets(QWidget* parent); + virtual SettingWidgets makeWidgets(QWidget* parent) = 0; /// Updates the widget returned by makeWidgets() to the current setting. /// /// \note If make_widgets() has not been called yet then nothing happens. - virtual void updateWidget(); + virtual void updateWidget() = 0; + + virtual void reset() = 0; const Category* getParent() const; const std::string& getKey() const; - const std::string& getLabel() const; + const QString& getLabel() const { return mLabel; } - int toInt() const; + int toInt() const { return getValueImpl(); } - double toDouble() const; + double toDouble() const { return getValueImpl(); } - std::string toString() const; + std::string toString() const { return getValueImpl(); } - bool isTrue() const; + bool isTrue() const { return getValueImpl(); } QColor toColor() const; }; + template + class TypedSetting : public Setting + { + public: + using Setting::Setting; + + void reset() final + { + resetValueImpl(); + updateWidget(); + } + + T getValue() const { return getValueImpl(); } + + void setValue(const T& value) { return setValueImpl(value); } + }; + // note: fullKeys have the format categoryKey/settingKey bool operator==(const Setting& setting, const std::string& fullKey); bool operator==(const std::string& fullKey, const Setting& setting); diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index a6f1da4f85..d6686d31d9 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -43,7 +43,7 @@ namespace CSMPrefs mEventHandler->removeShortcut(shortcut); } - bool ShortcutManager::getSequence(const std::string& name, QKeySequence& sequence) const + bool ShortcutManager::getSequence(std::string_view name, QKeySequence& sequence) const { SequenceMap::const_iterator item = mSequences.find(name); if (item != mSequences.end()) @@ -56,7 +56,7 @@ namespace CSMPrefs return false; } - void ShortcutManager::setSequence(const std::string& name, const QKeySequence& sequence) + void ShortcutManager::setSequence(std::string_view name, const QKeySequence& sequence) { // Add to map/modify SequenceMap::iterator item = mSequences.find(name); @@ -91,7 +91,7 @@ namespace CSMPrefs return false; } - void ShortcutManager::setModifier(const std::string& name, int modifier) + void ShortcutManager::setModifier(std::string_view name, int modifier) { // Add to map/modify ModifierMap::iterator item = mModifiers.find(name); diff --git a/apps/opencs/model/prefs/shortcutmanager.hpp b/apps/opencs/model/prefs/shortcutmanager.hpp index fc8db3f2b0..0cfe3ad86a 100644 --- a/apps/opencs/model/prefs/shortcutmanager.hpp +++ b/apps/opencs/model/prefs/shortcutmanager.hpp @@ -28,11 +28,11 @@ namespace CSMPrefs /// The shortcut class will do this automatically void removeShortcut(Shortcut* shortcut); - bool getSequence(const std::string& name, QKeySequence& sequence) const; - void setSequence(const std::string& name, const QKeySequence& sequence); + bool getSequence(std::string_view name, QKeySequence& sequence) const; + void setSequence(std::string_view name, const QKeySequence& sequence); bool getModifier(const std::string& name, int& modifier) const; - void setModifier(const std::string& name, int modifier); + void setModifier(std::string_view name, int modifier); std::string convertToString(const QKeySequence& sequence) const; std::string convertToString(int modifier) const; @@ -49,9 +49,9 @@ namespace CSMPrefs private: // Need a multimap in case multiple shortcuts share the same name - typedef std::multimap ShortcutMap; - typedef std::map SequenceMap; - typedef std::map ModifierMap; + typedef std::multimap> ShortcutMap; + typedef std::map> SequenceMap; + typedef std::map> ModifierMap; typedef std::map NameMap; typedef std::map KeyMap; diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index d8c71d7008..bdaf3a0fda 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -18,8 +18,9 @@ namespace CSMPrefs { - ShortcutSetting::ShortcutSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label) - : Setting(parent, mutex, key, label) + ShortcutSetting::ShortcutSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mButton(nullptr) , mEditorActive(false) , mEditorPos(0) @@ -30,14 +31,14 @@ namespace CSMPrefs } } - std::pair ShortcutSetting::makeWidgets(QWidget* parent) + SettingWidgets ShortcutSetting::makeWidgets(QWidget* parent) { QKeySequence sequence; State::get().getShortcutManager().getSequence(getKey(), sequence); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(sequence).c_str()); - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); @@ -50,14 +51,14 @@ namespace CSMPrefs connect(widget, &QPushButton::toggled, this, &ShortcutSetting::buttonToggled); - return std::make_pair(label, widget); + return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ShortcutSetting::updateWidget() { if (mButton) { - const std::string& shortcut = Settings::Manager::getString(getKey(), getParent()->getKey()); + const std::string shortcut = getValue(); QKeySequence sequence; State::get().getShortcutManager().convertFromString(shortcut, sequence); @@ -170,15 +171,7 @@ namespace CSMPrefs void ShortcutSetting::storeValue(const QKeySequence& sequence) { State::get().getShortcutManager().setSequence(getKey(), sequence); - - // Convert to string and assign - std::string value = State::get().getShortcutManager().convertToString(sequence); - - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), value); - } - + setValue(State::get().getShortcutManager().convertToString(sequence)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/shortcutsetting.hpp b/apps/opencs/model/prefs/shortcutsetting.hpp index bef140dada..bcb7b89488 100644 --- a/apps/opencs/model/prefs/shortcutsetting.hpp +++ b/apps/opencs/model/prefs/shortcutsetting.hpp @@ -2,6 +2,7 @@ #define CSM_PREFS_SHORTCUTSETTING_H #include +#include #include #include @@ -17,14 +18,16 @@ class QWidget; namespace CSMPrefs { class Category; - class ShortcutSetting : public Setting + + class ShortcutSetting final : public TypedSetting { Q_OBJECT public: - ShortcutSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label); + explicit ShortcutSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 97f29bc8be..f0af163bf2 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -24,50 +25,42 @@ #include "modifiersetting.hpp" #include "shortcutsetting.hpp" #include "stringsetting.hpp" +#include "values.hpp" CSMPrefs::State* CSMPrefs::State::sThis = nullptr; void CSMPrefs::State::declare() { declareCategory("Windows"); - declareInt("default-width", "Default window width", 800) + declareInt(mValues->mWindows.mDefaultWidth, "Default Window Width") .setTooltip("Newly opened top-level windows will open with this width.") .setMin(80); - declareInt("default-height", "Default window height", 600) + declareInt(mValues->mWindows.mDefaultHeight, "Default Window Height") .setTooltip("Newly opened top-level windows will open with this height.") .setMin(80); - declareBool("show-statusbar", "Show Status Bar", true) + declareBool(mValues->mWindows.mShowStatusbar, "Show Status Bar") .setTooltip( - "If a newly open top level window is showing status bars or not. " + "Whether a newly open top level window will show status bars. " " Note that this does not affect existing windows."); - declareSeparator(); - declareBool("reuse", "Reuse Subviews", true) + declareBool(mValues->mWindows.mReuse, "Reuse Subviews") .setTooltip( - "When a new subview is requested and a matching subview already " - " exist, do not open a new subview and use the existing one instead."); - declareInt("max-subviews", "Maximum number of subviews per top-level window", 256) + "When a new subview is requested and a matching subview already exists, reuse the existing subview."); + declareInt(mValues->mWindows.mMaxSubviews, "Maximum Number of Subviews per Top-Level Window") .setTooltip( "If the maximum number is reached and a new subview is opened " "it will be placed into a new top-level window.") .setRange(1, 256); - declareBool("hide-subview", "Hide single subview", false) + declareBool(mValues->mWindows.mHideSubview, "Hide Single Subview") .setTooltip( "When a view contains only a single subview, hide the subview title " "bar and if this subview is closed also close the view (unless it is the last " "view for this document)"); - declareInt("minimum-width", "Minimum subview width", 325) + declareInt(mValues->mWindows.mMinimumWidth, "Minimum Subview Width") .setTooltip("Minimum width of subviews.") .setRange(50, 10000); - declareSeparator(); - EnumValue scrollbarOnly("Scrollbar Only", - "Simple addition of scrollbars, the view window " - "does not grow automatically."); - declareEnum("mainwindow-scrollbar", "Horizontal scrollbar mode for main window.", scrollbarOnly) - .addValue(scrollbarOnly) - .addValue("Grow Only", "The view window grows as subviews are added. No scrollbars.") - .addValue("Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further."); + declareEnum(mValues->mWindows.mMainwindowScrollbar, "Main Window Horizontal Scrollbar Mode"); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - declareBool("grow-limit", "Grow Limit Screen", false) + declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen") .setTooltip( "When \"Grow then Scroll\" option is selected, the window size grows to" " the width of the virtual desktop. \nIf this option is selected the the window growth" @@ -75,378 +68,345 @@ void CSMPrefs::State::declare() #endif declareCategory("Records"); - EnumValue iconAndText("Icon and Text"); - EnumValues recordValues; - recordValues.add(iconAndText).add("Icon Only").add("Text Only"); - declareEnum("status-format", "Modification status display format", iconAndText).addValues(recordValues); - declareEnum("type-format", "ID type display format", iconAndText).addValues(recordValues); + declareEnum(mValues->mRecords.mStatusFormat, "Modification Status Display Format"); + declareEnum(mValues->mRecords.mTypeFormat, "ID Type Display Format"); declareCategory("ID Tables"); - EnumValue inPlaceEdit("Edit in Place", "Edit the clicked cell"); - EnumValue editRecord("Edit Record", "Open a dialogue subview for the clicked record"); - EnumValue view("View", "Open a scene subview for the clicked record (not available everywhere)"); - EnumValue editRecordAndClose("Edit Record and Close"); - EnumValues doubleClickValues; - doubleClickValues.add(inPlaceEdit) - .add(editRecord) - .add(view) - .add("Revert") - .add("Delete") - .add(editRecordAndClose) - .add("View and Close", "Open a scene subview for the clicked record and close the table subview"); - declareEnum("double", "Double Click", inPlaceEdit).addValues(doubleClickValues); - declareEnum("double-s", "Shift Double Click", editRecord).addValues(doubleClickValues); - declareEnum("double-c", "Control Double Click", view).addValues(doubleClickValues); - declareEnum("double-sc", "Shift Control Double Click", editRecordAndClose).addValues(doubleClickValues); - declareSeparator(); - EnumValue jumpAndSelect("Jump and Select", "Scroll new record into view and make it the selection"); - declareEnum("jump-to-added", "Action on adding or cloning a record", jumpAndSelect) - .addValue(jumpAndSelect) - .addValue("Jump Only", "Scroll new record into view") - .addValue("No Jump", "No special action"); - declareBool("extended-config", "Manually specify affected record types for an extended delete/revert", false) + declareEnum(mValues->mIdTables.mDouble, "Double Click"); + declareEnum(mValues->mIdTables.mDoubleS, "Shift Double Click"); + declareEnum(mValues->mIdTables.mDoubleC, "Control Double Click"); + declareEnum(mValues->mIdTables.mDoubleSc, "Shift Control Double Click"); + declareEnum(mValues->mIdTables.mJumpToAdded, "Action on Adding or Cloning a Record"); + declareBool( + mValues->mIdTables.mExtendedConfig, "Manually Specify Affected Record Types for an Extended Delete/Revert") .setTooltip( "Delete and revert commands have an extended form that also affects " "associated records.\n\n" "If this option is enabled, types of affected records are selected " "manually before a command execution.\nOtherwise, all associated " "records are deleted/reverted immediately."); - declareBool("subview-new-window", "Open Record in new window", false) + declareBool(mValues->mIdTables.mSubviewNewWindow, "Open Record in a New Window") .setTooltip( "When editing a record, open the view in a new window," " rather than docked in the main view."); + declareInt(mValues->mIdTables.mFilterDelay, "Filter Apply Delay (ms)"); declareCategory("ID Dialogues"); - declareBool("toolbar", "Show toolbar", true); + declareBool(mValues->mIdDialogues.mToolbar, "Show Toolbar"); declareCategory("Reports"); - EnumValue actionNone("None"); - EnumValue actionEdit("Edit", "Open a table or dialogue suitable for addressing the listed report"); - EnumValue actionRemove("Remove", "Remove the report from the report table"); - EnumValue actionEditAndRemove("Edit And Remove", - "Open a table or dialogue suitable for addressing the listed report, then remove the report from the report " - "table"); - EnumValues reportValues; - reportValues.add(actionNone).add(actionEdit).add(actionRemove).add(actionEditAndRemove); - declareEnum("double", "Double Click", actionEdit).addValues(reportValues); - declareEnum("double-s", "Shift Double Click", actionRemove).addValues(reportValues); - declareEnum("double-c", "Control Double Click", actionEditAndRemove).addValues(reportValues); - declareEnum("double-sc", "Shift Control Double Click", actionNone).addValues(reportValues); - declareBool("ignore-base-records", "Ignore base records in verifier", false); + declareEnum(mValues->mReports.mDouble, "Double Click"); + declareEnum(mValues->mReports.mDoubleS, "Shift Double Click"); + declareEnum(mValues->mReports.mDoubleC, "Control Double Click"); + declareEnum(mValues->mReports.mDoubleSc, "Shift Control Double Click"); + declareBool(mValues->mReports.mIgnoreBaseRecords, "Ignore Base Records in Verifier"); declareCategory("Search & Replace"); - declareInt("char-before", "Characters before search string", 10) - .setTooltip("Maximum number of character to display in search result before the searched text"); - declareInt("char-after", "Characters after search string", 10) - .setTooltip("Maximum number of character to display in search result after the searched text"); - declareBool("auto-delete", "Delete row from result table after a successful replace", true); + declareInt(mValues->mSearchAndReplace.mCharBefore, "Max Characters Before the Search String") + .setTooltip("Maximum number of characters to display in the search result before the searched text"); + declareInt(mValues->mSearchAndReplace.mCharAfter, "Max Characters After the Search String") + .setTooltip("Maximum number of characters to display in the search result after the searched text"); + declareBool(mValues->mSearchAndReplace.mAutoDelete, "Delete Row from the Result Table After Replace"); declareCategory("Scripts"); - declareBool("show-linenum", "Show Line Numbers", true) + declareBool(mValues->mScripts.mShowLinenum, "Show Line Numbers") .setTooltip( "Show line numbers to the left of the script editor window." "The current row and column numbers of the text cursor are shown at the bottom."); - declareBool("wrap-lines", "Wrap Lines", false).setTooltip("Wrap lines longer than width of script editor."); - declareBool("mono-font", "Use monospace font", true); - declareInt("tab-width", "Tab Width", 4).setTooltip("Number of characters for tab width").setRange(1, 10); - EnumValue warningsNormal("Normal", "Report warnings as warning"); - declareEnum("warnings", "Warning Mode", warningsNormal) - .addValue("Ignore", "Do not report warning") - .addValue(warningsNormal) - .addValue("Strict", "Promote warning to an error"); - declareBool("toolbar", "Show toolbar", true); - declareInt("compile-delay", "Delay between updating of source errors", 100) - .setTooltip("Delay in milliseconds") - .setRange(0, 10000); - declareInt("error-height", "Initial height of the error panel", 100).setRange(100, 10000); - declareBool("highlight-occurrences", "Highlight other occurrences of selected names", true); - declareColour("colour-highlight", "Colour of highlighted occurrences", QColor("lightcyan")); - declareSeparator(); - declareColour("colour-int", "Highlight Colour: Integer Literals", QColor("darkmagenta")); - declareColour("colour-float", "Highlight Colour: Float Literals", QColor("magenta")); - declareColour("colour-name", "Highlight Colour: Names", QColor("grey")); - declareColour("colour-keyword", "Highlight Colour: Keywords", QColor("red")); - declareColour("colour-special", "Highlight Colour: Special Characters", QColor("darkorange")); - declareColour("colour-comment", "Highlight Colour: Comments", QColor("green")); - declareColour("colour-id", "Highlight Colour: IDs", QColor("blue")); + declareBool(mValues->mScripts.mWrapLines, "Wrap Lines") + .setTooltip("Wrap lines that are longer than the width of the script editor."); + declareBool(mValues->mScripts.mMonoFont, "Use Monospace Font"); + declareInt(mValues->mScripts.mTabWidth, "Tab Width") + .setTooltip("Number of characters for tab width") + .setRange(1, 10); + declareEnum(mValues->mScripts.mWarnings, "Warning Mode"); + declareBool(mValues->mScripts.mToolbar, "Show Toolbar"); + declareInt(mValues->mScripts.mCompileDelay, "Source Error Update Delay (ms)").setRange(0, 10000); + declareInt(mValues->mScripts.mErrorHeight, "Initial Error Panel Height").setRange(100, 10000); + declareBool(mValues->mScripts.mHighlightOccurrences, "Highlight Selected Name Occurrences"); + declareColour(mValues->mScripts.mColourHighlight, "Highlight Colour: Selected Name Occurrences"); + declareColour(mValues->mScripts.mColourInt, "Highlight Colour: Integer Literals"); + declareColour(mValues->mScripts.mColourFloat, "Highlight Colour: Float Literals"); + declareColour(mValues->mScripts.mColourName, "Highlight Colour: Names"); + declareColour(mValues->mScripts.mColourKeyword, "Highlight Colour: Keywords"); + declareColour(mValues->mScripts.mColourSpecial, "Highlight Colour: Special Characters"); + declareColour(mValues->mScripts.mColourComment, "Highlight Colour: Comments"); + declareColour(mValues->mScripts.mColourId, "Highlight Colour: IDs"); declareCategory("General Input"); - declareBool("cycle", "Cyclic next/previous", false) + declareBool(mValues->mGeneralInput.mCycle, "Cyclic Next/Previous") .setTooltip( "When using next/previous functions at the last/first item of a " "list go to the first/last item"); declareCategory("3D Scene Input"); - declareDouble("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0); - declareDouble("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0); - declareSeparator(); + declareDouble(mValues->mSceneInput.mNaviWheelFactor, "Camera Zoom Sensitivity").setRange(-100.0, 100.0); + declareDouble(mValues->mSceneInput.mSNaviSensitivity, "Secondary Camera Movement Sensitivity") + .setRange(-1000.0, 1000.0); - declareDouble("p-navi-free-sensitivity", "Free Camera Sensitivity", 1 / 650.).setPrecision(5).setRange(0.0, 1.0); - declareBool("p-navi-free-invert", "Invert Free Camera Mouse Input", false); - declareDouble("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0); - declareDouble("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); - declareDouble("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0); - declareSeparator(); - - declareDouble("p-navi-orbit-sensitivity", "Orbit Camera Sensitivity", 1 / 650.).setPrecision(5).setRange(0.0, 1.0); - declareBool("p-navi-orbit-invert", "Invert Orbit Camera Mouse Input", false); - declareDouble("navi-orbit-rot-speed", "Orbital Camera Rotational Speed", 3.14 / 4).setRange(0.001, 6.28); - declareDouble("navi-orbit-speed-mult", "Orbital Camera Speed Multiplier (from Modifier)", 4) + declareDouble(mValues->mSceneInput.mPNaviFreeSensitivity, "Free Camera Sensitivity") + .setPrecision(5) + .setRange(0.0, 1.0); + declareBool(mValues->mSceneInput.mPNaviFreeInvert, "Invert Free Camera Mouse Input"); + declareDouble(mValues->mSceneInput.mNaviFreeLinSpeed, "Free Camera Linear Speed").setRange(1.0, 10000.0); + declareDouble(mValues->mSceneInput.mNaviFreeRotSpeed, "Free Camera Rotational Speed").setRange(0.001, 6.28); + declareDouble(mValues->mSceneInput.mNaviFreeSpeedMult, "Free Camera Speed Multiplier (from Modifier)") .setRange(0.001, 1000.0); - declareBool("navi-orbit-const-roll", "Keep camera roll constant for orbital camera", true); - declareSeparator(); - declareBool("context-select", "Context Sensitive Selection", false); - declareDouble("drag-factor", "Mouse sensitivity during drag operations", 1.0).setRange(0.001, 100.0); - declareDouble("drag-wheel-factor", "Mouse wheel sensitivity during drag operations", 1.0).setRange(0.001, 100.0); - declareDouble("drag-shift-factor", "Shift-acceleration factor during drag operations", 4.0) + declareDouble(mValues->mSceneInput.mPNaviOrbitSensitivity, "Orbit Camera Sensitivity") + .setPrecision(5) + .setRange(0.0, 1.0); + declareBool(mValues->mSceneInput.mPNaviOrbitInvert, "Invert Orbit Camera Mouse Input"); + declareDouble(mValues->mSceneInput.mNaviOrbitRotSpeed, "Orbital Camera Rotational Speed").setRange(0.001, 6.28); + declareDouble(mValues->mSceneInput.mNaviOrbitSpeedMult, "Orbital Camera Speed Multiplier (from Modifier)") + .setRange(0.001, 1000.0); + declareBool(mValues->mSceneInput.mNaviOrbitConstRoll, "Keep Camera Roll Constant for Orbital Camera"); + + declareBool(mValues->mSceneInput.mContextSelect, "Context Sensitive Selection"); + declareDouble(mValues->mSceneInput.mDragFactor, "Dragging Mouse Sensitivity").setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mDragWheelFactor, "Dragging Mouse Wheel Sensitivity").setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mDragShiftFactor, "Dragging Shift-Acceleration Factor") .setTooltip("Acceleration factor during drag operations while holding down shift") .setRange(0.001, 100.0); - declareDouble("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); + declareDouble(mValues->mSceneInput.mRotateFactor, "Free rotation factor").setPrecision(4).setRange(0.0001, 0.1); declareCategory("Rendering"); - declareInt("framerate-limit", "FPS limit", 60) + declareInt(mValues->mRendering.mFramerateLimit, "FPS Limit") .setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\".") .setRange(0, 10000); - declareInt("camera-fov", "Camera FOV", 90).setRange(10, 170); - declareBool("camera-ortho", "Orthographic projection for camera", false); - declareInt("camera-ortho-size", "Orthographic projection size parameter", 100) + declareInt(mValues->mRendering.mCameraFov, "Camera FOV").setRange(10, 170); + declareBool(mValues->mRendering.mCameraOrtho, "Orthographic Projection for Camera"); + declareInt(mValues->mRendering.mCameraOrthoSize, "Orthographic Projection Size Parameter") .setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world.") .setRange(10, 10000); - declareDouble("object-marker-alpha", "Object Marker Transparency", 0.5).setPrecision(2).setRange(0, 1); - declareBool("scene-use-gradient", "Use Gradient Background", true); - declareColour("scene-day-background-colour", "Day Background Colour", QColor(110, 120, 128, 255)); - declareColour("scene-day-gradient-colour", "Day Gradient Colour", QColor(47, 51, 51, 255)) + declareDouble(mValues->mRendering.mObjectMarkerAlpha, "Object Marker Transparency").setPrecision(2).setRange(0, 1); + declareBool(mValues->mRendering.mSceneUseGradient, "Use Gradient Background"); + declareColour(mValues->mRendering.mSceneDayBackgroundColour, "Day Background Colour"); + declareColour(mValues->mRendering.mSceneDayGradientColour, "Day Gradient Colour") .setTooltip( "Sets the gradient color to use in conjunction with the day background color. Ignored if " "the gradient option is disabled."); - declareColour("scene-bright-background-colour", "Scene Bright Background Colour", QColor(79, 87, 92, 255)); - declareColour("scene-bright-gradient-colour", "Scene Bright Gradient Colour", QColor(47, 51, 51, 255)) + declareColour(mValues->mRendering.mSceneBrightBackgroundColour, "Scene Bright Background Colour"); + declareColour(mValues->mRendering.mSceneBrightGradientColour, "Scene Bright Gradient Colour") .setTooltip( "Sets the gradient color to use in conjunction with the bright background color. Ignored if " "the gradient option is disabled."); - declareColour("scene-night-background-colour", "Scene Night Background Colour", QColor(64, 77, 79, 255)); - declareColour("scene-night-gradient-colour", "Scene Night Gradient Colour", QColor(47, 51, 51, 255)) + declareColour(mValues->mRendering.mSceneNightBackgroundColour, "Scene Night Background Colour"); + declareColour(mValues->mRendering.mSceneNightGradientColour, "Scene Night Gradient Colour") .setTooltip( "Sets the gradient color to use in conjunction with the night background color. Ignored if " "the gradient option is disabled."); - declareBool("scene-day-night-switch-nodes", "Use Day/Night Switch Nodes", true); + declareBool(mValues->mRendering.mSceneDayNightSwitchNodes, "Use Day/Night Switch Nodes"); declareCategory("Tooltips"); - declareBool("scene", "Show Tooltips in 3D scenes", true); - declareBool("scene-hide-basic", "Hide basic 3D scenes tooltips", false); - declareInt("scene-delay", "Tooltip delay in milliseconds", 500).setMin(1); - - EnumValue createAndInsert("Create cell and insert"); - EnumValue showAndInsert("Show cell and insert"); - EnumValue dontInsert("Discard"); - EnumValue insertAnyway("Insert anyway"); - EnumValues insertOutsideCell; - insertOutsideCell.add(createAndInsert).add(dontInsert).add(insertAnyway); - EnumValues insertOutsideVisibleCell; - insertOutsideVisibleCell.add(showAndInsert).add(dontInsert).add(insertAnyway); - - EnumValue createAndLandEdit("Create cell and land, then edit"); - EnumValue showAndLandEdit("Show cell and edit"); - EnumValue dontLandEdit("Discard"); - EnumValues landeditOutsideCell; - landeditOutsideCell.add(createAndLandEdit).add(dontLandEdit); - EnumValues landeditOutsideVisibleCell; - landeditOutsideVisibleCell.add(showAndLandEdit).add(dontLandEdit); - - EnumValue SelectOnly("Select only"); - EnumValue SelectAdd("Add to selection"); - EnumValue SelectRemove("Remove from selection"); - EnumValue selectInvert("Invert selection"); - EnumValues primarySelectAction; - primarySelectAction.add(SelectOnly).add(SelectAdd).add(SelectRemove).add(selectInvert); - EnumValues secondarySelectAction; - secondarySelectAction.add(SelectOnly).add(SelectAdd).add(SelectRemove).add(selectInvert); + declareBool(mValues->mTooltips.mScene, "Show Tooltips in 3D Scenes"); + declareBool(mValues->mTooltips.mSceneHideBasic, "Hide Basic 3D Scene Tooltips"); + declareInt(mValues->mTooltips.mSceneDelay, "Tooltip Delay (ms)").setMin(1); declareCategory("3D Scene Editing"); - declareDouble("gridsnap-movement", "Grid snap size", 16); - declareDouble("gridsnap-rotation", "Angle snap size", 15); - declareDouble("gridsnap-scale", "Scale snap size", 0.25); - declareInt("distance", "Drop Distance", 50) + declareDouble(mValues->mSceneEditing.mGridsnapMovement, "Grid Snap Size"); + declareDouble(mValues->mSceneEditing.mGridsnapRotation, "Angle Snap Size"); + declareDouble(mValues->mSceneEditing.mGridsnapScale, "Scale Snap Size"); + declareInt(mValues->mSceneEditing.mDistance, "Drop Distance") .setTooltip( - "If an instance drop can not be placed against another object at the " - "insert point, it will be placed by this distance from the insert point instead"); - declareEnum("outside-drop", "Handling drops outside of cells", createAndInsert).addValues(insertOutsideCell); - declareEnum("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert) - .addValues(insertOutsideVisibleCell); - declareEnum("outside-landedit", "Handling terrain edit outside of cells", createAndLandEdit) - .setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record.") - .addValues(landeditOutsideCell); - declareEnum("outside-visible-landedit", "Handling terrain edit outside of visible cells", showAndLandEdit) - .setTooltip("Behavior of terrain editing, if land editing brush reaches an area that is not currently visible.") - .addValues(landeditOutsideVisibleCell); - declareInt("texturebrush-maximumsize", "Maximum texture brush size", 50).setMin(1); - declareInt("shapebrush-maximumsize", "Maximum height edit brush size", 100) + "If the dropped instance cannot be placed against another object at the " + "insertion point, it will be placed at this distance from the insertion point."); + declareEnum(mValues->mSceneEditing.mOutsideDrop, "Instance Dropping Outside of Cells"); + declareEnum(mValues->mSceneEditing.mOutsideVisibleDrop, "Instance Dropping Outside of Visible Cells"); + declareEnum(mValues->mSceneEditing.mOutsideLandedit, "Terrain Editing Outside of Cells") + .setTooltip("Behaviour of terrain editing if land editing brush reaches an area without a cell record."); + declareEnum(mValues->mSceneEditing.mOutsideVisibleLandedit, "Terrain Editing Outside of Visible Cells") + .setTooltip( + "Behaviour of terrain editing if land editing brush reaches an area that is not currently visible."); + declareInt(mValues->mSceneEditing.mTexturebrushMaximumsize, "Maximum Texture Brush Size").setMin(1); + declareInt(mValues->mSceneEditing.mShapebrushMaximumsize, "Maximum Height Edit Brush Size") .setTooltip("Setting for the slider range of brush size in terrain height editing.") .setMin(1); - declareBool("landedit-post-smoothpainting", "Smooth land after painting height", false) - .setTooltip("Raise and lower tools will leave bumpy finish without this option"); - declareDouble("landedit-post-smoothstrength", "Smoothing strength (post-edit)", 0.25) + declareBool(mValues->mSceneEditing.mLandeditPostSmoothpainting, "Smooth Land after Height Painting") + .setTooltip("Smooth the normally bumpy results of raise and lower tools."); + declareDouble(mValues->mSceneEditing.mLandeditPostSmoothstrength, "Post-Edit Smoothing Strength") .setTooltip( - "If smoothing land after painting height is used, this is the percentage of smooth applied afterwards. " - "Negative values may be used to roughen instead of smooth.") + "Smoothing strength for Smooth Land after Height Painting setting. " + "Negative values may be used to invert the effect and make the terrain rougher.") .setMin(-1) .setMax(1); - declareBool("open-list-view", "Open displays list view", false) + declareBool(mValues->mSceneEditing.mOpenListView, "Open Action Shows Instances Table") .setTooltip( - "When opening a reference from the scene view, it will open the" - " instance list view instead of the individual instance record view."); - declareEnum("primary-select-action", "Action for primary select", SelectOnly) + "Opening an instance from the scene view will open the instances table instead of the record view for that " + "instance."); + declareEnum(mValues->mSceneEditing.mPrimarySelectAction, "Primary Select Action") .setTooltip( "Selection can be chosen between select only, add to selection, remove from selection and invert " - "selection.") - .addValues(primarySelectAction); - declareEnum("secondary-select-action", "Action for secondary select", SelectAdd) + "selection."); + declareEnum(mValues->mSceneEditing.mSecondarySelectAction, "Secondary Select Action") .setTooltip( "Selection can be chosen between select only, add to selection, remove from selection and invert " - "selection.") - .addValues(secondarySelectAction); + "selection."); declareCategory("Key Bindings"); declareSubcategory("Document"); - declareShortcut("document-file-newgame", "New Game", QKeySequence(Qt::ControlModifier | Qt::Key_N)); - declareShortcut("document-file-newaddon", "New Addon", QKeySequence()); - declareShortcut("document-file-open", "Open", QKeySequence(Qt::ControlModifier | Qt::Key_O)); - declareShortcut("document-file-save", "Save", QKeySequence(Qt::ControlModifier | Qt::Key_S)); - declareShortcut("document-help-help", "Help", QKeySequence(Qt::Key_F1)); - declareShortcut("document-help-tutorial", "Tutorial", QKeySequence()); - declareShortcut("document-file-verify", "Verify", QKeySequence()); - declareShortcut("document-file-merge", "Merge", QKeySequence()); - declareShortcut("document-file-errorlog", "Open Load Error Log", QKeySequence()); - declareShortcut("document-file-metadata", "Meta Data", QKeySequence()); - declareShortcut("document-file-close", "Close Document", QKeySequence(Qt::ControlModifier | Qt::Key_W)); - declareShortcut("document-file-exit", "Exit Application", QKeySequence(Qt::ControlModifier | Qt::Key_Q)); - declareShortcut("document-edit-undo", "Undo", QKeySequence(Qt::ControlModifier | Qt::Key_Z)); - declareShortcut("document-edit-redo", "Redo", QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Z)); - declareShortcut("document-edit-preferences", "Open Preferences", QKeySequence()); - declareShortcut("document-edit-search", "Search", QKeySequence(Qt::ControlModifier | Qt::Key_F)); - declareShortcut("document-view-newview", "New View", QKeySequence()); - declareShortcut("document-view-statusbar", "Toggle Status Bar", QKeySequence()); - declareShortcut("document-view-filters", "Open Filter List", QKeySequence()); - declareShortcut("document-world-regions", "Open Region List", QKeySequence()); - declareShortcut("document-world-cells", "Open Cell List", QKeySequence()); - declareShortcut("document-world-referencables", "Open Object List", QKeySequence()); - declareShortcut("document-world-references", "Open Instance List", QKeySequence()); - declareShortcut("document-world-lands", "Open Lands List", QKeySequence()); - declareShortcut("document-world-landtextures", "Open Land Textures List", QKeySequence()); - declareShortcut("document-world-pathgrid", "Open Pathgrid List", QKeySequence()); - declareShortcut("document-world-regionmap", "Open Region Map", QKeySequence()); - declareShortcut("document-mechanics-globals", "Open Global List", QKeySequence()); - declareShortcut("document-mechanics-gamesettings", "Open Game Settings", QKeySequence()); - declareShortcut("document-mechanics-scripts", "Open Script List", QKeySequence()); - declareShortcut("document-mechanics-spells", "Open Spell List", QKeySequence()); - declareShortcut("document-mechanics-enchantments", "Open Enchantment List", QKeySequence()); - declareShortcut("document-mechanics-magiceffects", "Open Magic Effect List", QKeySequence()); - declareShortcut("document-mechanics-startscripts", "Open Start Script List", QKeySequence()); - declareShortcut("document-character-skills", "Open Skill List", QKeySequence()); - declareShortcut("document-character-classes", "Open Class List", QKeySequence()); - declareShortcut("document-character-factions", "Open Faction List", QKeySequence()); - declareShortcut("document-character-races", "Open Race List", QKeySequence()); - declareShortcut("document-character-birthsigns", "Open Birthsign List", QKeySequence()); - declareShortcut("document-character-topics", "Open Topic List", QKeySequence()); - declareShortcut("document-character-journals", "Open Journal List", QKeySequence()); - declareShortcut("document-character-topicinfos", "Open Topic Info List", QKeySequence()); - declareShortcut("document-character-journalinfos", "Open Journal Info List", QKeySequence()); - declareShortcut("document-character-bodyparts", "Open Body Part List", QKeySequence()); - declareShortcut("document-assets-reload", "Reload Assets", QKeySequence(Qt::Key_F5)); - declareShortcut("document-assets-sounds", "Open Sound Asset List", QKeySequence()); - declareShortcut("document-assets-soundgens", "Open Sound Generator List", QKeySequence()); - declareShortcut("document-assets-meshes", "Open Mesh Asset List", QKeySequence()); - declareShortcut("document-assets-icons", "Open Icon Asset List", QKeySequence()); - declareShortcut("document-assets-music", "Open Music Asset List", QKeySequence()); - declareShortcut("document-assets-soundres", "Open Sound File List", QKeySequence()); - declareShortcut("document-assets-textures", "Open Texture Asset List", QKeySequence()); - declareShortcut("document-assets-videos", "Open Video Asset List", QKeySequence()); - declareShortcut("document-debug-run", "Run Debug", QKeySequence()); - declareShortcut("document-debug-shutdown", "Stop Debug", QKeySequence()); - declareShortcut("document-debug-profiles", "Debug Profiles", QKeySequence()); - declareShortcut("document-debug-runlog", "Open Run Log", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mDocumentFileNewgame, "New Game"); + declareShortcut(mValues->mKeyBindings.mDocumentFileNewaddon, "New Addon"); + declareShortcut(mValues->mKeyBindings.mDocumentFileOpen, "Open"); + declareShortcut(mValues->mKeyBindings.mDocumentFileSave, "Save"); + declareShortcut(mValues->mKeyBindings.mDocumentHelpHelp, "Help"); + declareShortcut(mValues->mKeyBindings.mDocumentHelpTutorial, "Tutorial"); + declareShortcut(mValues->mKeyBindings.mDocumentFileVerify, "Verify"); + declareShortcut(mValues->mKeyBindings.mDocumentFileMerge, "Merge"); + declareShortcut(mValues->mKeyBindings.mDocumentFileErrorlog, "Open Load Error Log"); + declareShortcut(mValues->mKeyBindings.mDocumentFileMetadata, "Meta Data"); + declareShortcut(mValues->mKeyBindings.mDocumentFileClose, "Close Document"); + declareShortcut(mValues->mKeyBindings.mDocumentFileExit, "Exit Application"); + declareShortcut(mValues->mKeyBindings.mDocumentEditUndo, "Undo"); + declareShortcut(mValues->mKeyBindings.mDocumentEditRedo, "Redo"); + declareShortcut(mValues->mKeyBindings.mDocumentEditPreferences, "Open Preferences"); + declareShortcut(mValues->mKeyBindings.mDocumentEditSearch, "Search"); + declareShortcut(mValues->mKeyBindings.mDocumentViewNewview, "New View"); + declareShortcut(mValues->mKeyBindings.mDocumentViewStatusbar, "Toggle Status Bar"); + declareShortcut(mValues->mKeyBindings.mDocumentViewFilters, "Open Filter List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldRegions, "Open Region List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldCells, "Open Cell List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldReferencables, "Open Object List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldReferences, "Open Instance List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldLands, "Open Lands List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldLandtextures, "Open Land Textures List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldPathgrid, "Open Pathgrid List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldRegionmap, "Open Region Map"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsGlobals, "Open Global List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsGamesettings, "Open Game Settings"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsScripts, "Open Script List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsSpells, "Open Spell List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsEnchantments, "Open Enchantment List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsMagiceffects, "Open Magic Effect List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsStartscripts, "Open Start Script List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterSkills, "Open Skill List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterClasses, "Open Class List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterFactions, "Open Faction List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterRaces, "Open Race List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterBirthsigns, "Open Birthsign List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterTopics, "Open Topic List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterJournals, "Open Journal List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterTopicinfos, "Open Topic Info List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterJournalinfos, "Open Journal Info List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterBodyparts, "Open Body Part List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsReload, "Reload Assets"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSounds, "Open Sound Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSoundgens, "Open Sound Generator List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsMeshes, "Open Mesh Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsIcons, "Open Icon Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsMusic, "Open Music Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSoundres, "Open Sound File List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsTextures, "Open Texture Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsVideos, "Open Video Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugRun, "Run Debug"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugShutdown, "Stop Debug"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugProfiles, "Debug Profiles"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugRunlog, "Open Run Log"); declareSubcategory("Table"); - declareShortcut("table-edit", "Edit Record", QKeySequence()); - declareShortcut("table-add", "Add Row/Record", QKeySequence(Qt::ShiftModifier | Qt::Key_A)); - declareShortcut("table-clone", "Clone Record", QKeySequence(Qt::ShiftModifier | Qt::Key_D)); - declareShortcut("touch-record", "Touch Record", QKeySequence()); - declareShortcut("table-revert", "Revert Record", QKeySequence()); - declareShortcut("table-remove", "Remove Row/Record", QKeySequence(Qt::Key_Delete)); - declareShortcut("table-moveup", "Move Record Up", QKeySequence()); - declareShortcut("table-movedown", "Move Record Down", QKeySequence()); - declareShortcut("table-view", "View Record", QKeySequence(Qt::ShiftModifier | Qt::Key_C)); - declareShortcut("table-preview", "Preview Record", QKeySequence(Qt::ShiftModifier | Qt::Key_V)); - declareShortcut("table-extendeddelete", "Extended Record Deletion", QKeySequence()); - declareShortcut("table-extendedrevert", "Extended Record Revertion", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mTableEdit, "Edit Record"); + declareShortcut(mValues->mKeyBindings.mTableAdd, "Add Row/Record"); + declareShortcut(mValues->mKeyBindings.mTableClone, "Clone Record"); + declareShortcut(mValues->mKeyBindings.mTouchRecord, "Touch Record"); + declareShortcut(mValues->mKeyBindings.mTableRevert, "Revert Record"); + declareShortcut(mValues->mKeyBindings.mTableRemove, "Remove Row/Record"); + declareShortcut(mValues->mKeyBindings.mTableMoveup, "Move Record Up"); + declareShortcut(mValues->mKeyBindings.mTableMovedown, "Move Record Down"); + declareShortcut(mValues->mKeyBindings.mTableView, "View Record"); + declareShortcut(mValues->mKeyBindings.mTablePreview, "Preview Record"); + declareShortcut(mValues->mKeyBindings.mTableExtendeddelete, "Extended Record Deletion"); + declareShortcut(mValues->mKeyBindings.mTableExtendedrevert, "Extended Record Revertion"); declareSubcategory("Report Table"); - declareShortcut("reporttable-show", "Show Report", QKeySequence()); - declareShortcut("reporttable-remove", "Remove Report", QKeySequence(Qt::Key_Delete)); - declareShortcut("reporttable-replace", "Replace Report", QKeySequence()); - declareShortcut("reporttable-refresh", "Refresh Report", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mReporttableShow, "Show Report"); + declareShortcut(mValues->mKeyBindings.mReporttableRemove, "Remove Report"); + declareShortcut(mValues->mKeyBindings.mReporttableReplace, "Replace Report"); + declareShortcut(mValues->mKeyBindings.mReporttableRefresh, "Refresh Report"); declareSubcategory("Scene"); - declareShortcut("scene-navi-primary", "Camera Rotation From Mouse Movement", QKeySequence(Qt::LeftButton)); - declareShortcut("scene-navi-secondary", "Camera Translation From Mouse Movement", - QKeySequence(Qt::ControlModifier | (int)Qt::LeftButton)); - declareShortcut("scene-open-primary", "Primary Open", QKeySequence(Qt::ShiftModifier | (int)Qt::LeftButton)); - declareShortcut("scene-edit-primary", "Primary Edit", QKeySequence(Qt::RightButton)); - declareShortcut("scene-edit-secondary", "Secondary Edit", QKeySequence(Qt::ControlModifier | (int)Qt::RightButton)); - declareShortcut("scene-select-primary", "Primary Select", QKeySequence(Qt::MiddleButton)); - declareShortcut( - "scene-select-secondary", "Secondary Select", QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton)); - declareShortcut( - "scene-select-tertiary", "Tertiary Select", QKeySequence(Qt::ShiftModifier | (int)Qt::MiddleButton)); - declareModifier("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift); - declareShortcut("scene-delete", "Delete Instance", QKeySequence(Qt::Key_Delete)); - declareShortcut("scene-instance-drop-terrain", "Drop to terrain level", QKeySequence(Qt::Key_G)); - declareShortcut("scene-instance-drop-collision", "Drop to collision", QKeySequence(Qt::Key_H)); - declareShortcut("scene-instance-drop-terrain-separately", "Drop to terrain level separately", QKeySequence()); - declareShortcut("scene-instance-drop-collision-separately", "Drop to collision separately", QKeySequence()); - declareShortcut("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5)); - declareShortcut("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6)); - declareShortcut("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8)); - declareShortcut("scene-load-cam-westcell", "Load West Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_4)); - declareShortcut("scene-load-cam-southcell", "Load South Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_2)); - declareShortcut("scene-edit-abort", "Abort", QKeySequence(Qt::Key_Escape)); - declareShortcut("scene-focus-toolbar", "Toggle Toolbar Focus", QKeySequence(Qt::Key_T)); - declareShortcut("scene-render-stats", "Debug Rendering Stats", QKeySequence(Qt::Key_F3)); + declareShortcut(mValues->mKeyBindings.mSceneNaviPrimary, "Camera Rotation From Mouse Movement"); + declareShortcut(mValues->mKeyBindings.mSceneNaviSecondary, "Camera Translation From Mouse Movement"); + declareShortcut(mValues->mKeyBindings.mSceneOpenPrimary, "Primary Open"); + declareShortcut(mValues->mKeyBindings.mSceneEditPrimary, "Primary Edit"); + declareShortcut(mValues->mKeyBindings.mSceneEditSecondary, "Secondary Edit"); + declareShortcut(mValues->mKeyBindings.mSceneSelectPrimary, "Primary Select"); + declareShortcut(mValues->mKeyBindings.mSceneSelectSecondary, "Secondary Select"); + declareShortcut(mValues->mKeyBindings.mSceneSelectTertiary, "Tertiary Select"); + declareModifier(mValues->mKeyBindings.mSceneSpeedModifier, "Speed Modifier"); + declareShortcut(mValues->mKeyBindings.mSceneDelete, "Delete Instance"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDrop, "Drop to Collision"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamCell, "Load Camera Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamEastcell, "Load East Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamNorthcell, "Load North Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamWestcell, "Load West Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamSouthcell, "Load South Cell"); + declareShortcut(mValues->mKeyBindings.mSceneEditAbort, "Abort"); + declareShortcut(mValues->mKeyBindings.mSceneFocusToolbar, "Toggle Toolbar Focus"); + declareShortcut(mValues->mKeyBindings.mSceneRenderStats, "Debug Rendering Stats"); + declareShortcut(mValues->mKeyBindings.mSceneDuplicate, "Duplicate Instance"); + declareShortcut(mValues->mKeyBindings.mSceneClearSelection, "Clear Selection"); + declareShortcut(mValues->mKeyBindings.mSceneUnhideAll, "Unhide All Objects"); + declareShortcut(mValues->mKeyBindings.mSceneToggleVisibility, "Toggle Selection Visibility"); + declareShortcut(mValues->mKeyBindings.mSceneGroup0, "Selection Group 0"); + declareShortcut(mValues->mKeyBindings.mSceneSave0, "Save Group 0"); + declareShortcut(mValues->mKeyBindings.mSceneGroup1, "Select Group 1"); + declareShortcut(mValues->mKeyBindings.mSceneSave1, "Save Group 1"); + declareShortcut(mValues->mKeyBindings.mSceneGroup2, "Select Group 2"); + declareShortcut(mValues->mKeyBindings.mSceneSave2, "Save Group 2"); + declareShortcut(mValues->mKeyBindings.mSceneGroup3, "Select Group 3"); + declareShortcut(mValues->mKeyBindings.mSceneSave3, "Save Group 3"); + declareShortcut(mValues->mKeyBindings.mSceneGroup4, "Select Group 4"); + declareShortcut(mValues->mKeyBindings.mSceneSave4, "Save Group 4"); + declareShortcut(mValues->mKeyBindings.mSceneGroup5, "Selection Group 5"); + declareShortcut(mValues->mKeyBindings.mSceneSave5, "Save Group 5"); + declareShortcut(mValues->mKeyBindings.mSceneGroup6, "Selection Group 6"); + declareShortcut(mValues->mKeyBindings.mSceneSave6, "Save Group 6"); + declareShortcut(mValues->mKeyBindings.mSceneGroup7, "Selection Group 7"); + declareShortcut(mValues->mKeyBindings.mSceneSave7, "Save Group 7"); + declareShortcut(mValues->mKeyBindings.mSceneGroup8, "Selection Group 8"); + declareShortcut(mValues->mKeyBindings.mSceneSave8, "Save Group 8"); + declareShortcut(mValues->mKeyBindings.mSceneGroup9, "Selection Group 9"); + declareShortcut(mValues->mKeyBindings.mSceneSave9, "Save Group 9"); + declareShortcut(mValues->mKeyBindings.mSceneAxisX, "X Axis Movement Lock"); + declareShortcut(mValues->mKeyBindings.mSceneAxisY, "Y Axis Movement Lock"); + declareShortcut(mValues->mKeyBindings.mSceneAxisZ, "Z Axis Movement Lock"); + declareShortcut(mValues->mKeyBindings.mSceneMoveSubmode, "Move Object Submode"); + declareShortcut(mValues->mKeyBindings.mSceneScaleSubmode, "Scale Object Submode"); + declareShortcut(mValues->mKeyBindings.mSceneRotateSubmode, "Rotate Object Submode"); + declareShortcut(mValues->mKeyBindings.mSceneCameraCycle, "Cycle Camera Mode"); declareSubcategory("1st/Free Camera"); - declareShortcut("free-forward", "Forward", QKeySequence(Qt::Key_W)); - declareShortcut("free-backward", "Backward", QKeySequence(Qt::Key_S)); - declareShortcut("free-left", "Left", QKeySequence(Qt::Key_A)); - declareShortcut("free-right", "Right", QKeySequence(Qt::Key_D)); - declareShortcut("free-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); - declareShortcut("free-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); - declareShortcut("free-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); + declareShortcut(mValues->mKeyBindings.mFreeForward, "Forward"); + declareShortcut(mValues->mKeyBindings.mFreeBackward, "Backward"); + declareShortcut(mValues->mKeyBindings.mFreeLeft, "Left"); + declareShortcut(mValues->mKeyBindings.mFreeRight, "Right"); + declareShortcut(mValues->mKeyBindings.mFreeRollLeft, "Roll Left"); + declareShortcut(mValues->mKeyBindings.mFreeRollRight, "Roll Right"); + declareShortcut(mValues->mKeyBindings.mFreeSpeedMode, "Toggle Speed Mode"); declareSubcategory("Orbit Camera"); - declareShortcut("orbit-up", "Up", QKeySequence(Qt::Key_W)); - declareShortcut("orbit-down", "Down", QKeySequence(Qt::Key_S)); - declareShortcut("orbit-left", "Left", QKeySequence(Qt::Key_A)); - declareShortcut("orbit-right", "Right", QKeySequence(Qt::Key_D)); - declareShortcut("orbit-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); - declareShortcut("orbit-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); - declareShortcut("orbit-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); - declareShortcut("orbit-center-selection", "Center On Selected", QKeySequence(Qt::Key_C)); + declareShortcut(mValues->mKeyBindings.mOrbitUp, "Up"); + declareShortcut(mValues->mKeyBindings.mOrbitDown, "Down"); + declareShortcut(mValues->mKeyBindings.mOrbitLeft, "Left"); + declareShortcut(mValues->mKeyBindings.mOrbitRight, "Right"); + declareShortcut(mValues->mKeyBindings.mOrbitRollLeft, "Roll Left"); + declareShortcut(mValues->mKeyBindings.mOrbitRollRight, "Roll Right"); + declareShortcut(mValues->mKeyBindings.mOrbitSpeedMode, "Toggle Speed Mode"); + declareShortcut(mValues->mKeyBindings.mOrbitCenterSelection, "Center On Selected"); declareSubcategory("Script Editor"); - declareShortcut("script-editor-comment", "Comment Selection", QKeySequence()); - declareShortcut("script-editor-uncomment", "Uncomment Selection", QKeySequence()); + declareShortcut(mValues->mKeyBindings.mScriptEditorComment, "Comment Selection"); + declareShortcut(mValues->mKeyBindings.mScriptEditorUncomment, "Uncomment Selection"); declareCategory("Models"); - declareString("baseanim", "base animations", "meshes/base_anim.nif") - .setTooltip("3rd person base model with textkeys-data"); - declareString("baseanimkna", "base animations, kna", "meshes/base_animkna.nif") - .setTooltip("3rd person beast race base model with textkeys-data"); - declareString("baseanimfemale", "base animations, female", "meshes/base_anim_female.nif") - .setTooltip("3rd person female base model with textkeys-data"); - declareString("wolfskin", "base animations, wolf", "meshes/wolf/skin.nif").setTooltip("3rd person werewolf skin"); + declareString(mValues->mModels.mBaseanim, "Base Animations").setTooltip("Third person base model and animations"); + declareString(mValues->mModels.mBaseanimkna, "Base Animations, Beast") + .setTooltip("Third person beast race base model and animations"); + declareString(mValues->mModels.mBaseanimfemale, "Base Animations, Female") + .setTooltip("Third person female base model and animations"); + declareString(mValues->mModels.mWolfskin, "Base Animations, Werewolf").setTooltip("Third person werewolf skin"); } void CSMPrefs::State::declareCategory(const std::string& key) @@ -463,71 +423,52 @@ void CSMPrefs::State::declareCategory(const std::string& key) } } -CSMPrefs::IntSetting& CSMPrefs::State::declareInt(const std::string& key, const std::string& label, int default_) +CSMPrefs::IntSetting& CSMPrefs::State::declareInt(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, std::to_string(default_)); - - default_ = Settings::Manager::getInt(key, mCurrentCategory->second.getKey()); - - CSMPrefs::IntSetting* setting = new CSMPrefs::IntSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + CSMPrefs::IntSetting* setting + = new CSMPrefs::IntSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble( - const std::string& key, const std::string& label, double default_) +CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - std::ostringstream stream; - stream << default_; - setDefault(key, stream.str()); - - default_ = Settings::Manager::getFloat(key, mCurrentCategory->second.getKey()); - CSMPrefs::DoubleSetting* setting - = new CSMPrefs::DoubleSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::DoubleSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::BoolSetting& CSMPrefs::State::declareBool(const std::string& key, const std::string& label, bool default_) +CSMPrefs::BoolSetting& CSMPrefs::State::declareBool(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_ ? "true" : "false"); - - default_ = Settings::Manager::getBool(key, mCurrentCategory->second.getKey()); - CSMPrefs::BoolSetting* setting - = new CSMPrefs::BoolSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::BoolSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum( - const std::string& key, const std::string& label, EnumValue default_) +CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum(EnumSettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_.mValue); - - default_.mValue = Settings::Manager::getString(key, mCurrentCategory->second.getKey()); - - CSMPrefs::EnumSetting* setting - = new CSMPrefs::EnumSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + CSMPrefs::EnumSetting* setting = new CSMPrefs::EnumSetting( + &mCurrentCategory->second, &mMutex, value.getValue().mName, label, value.getEnumValues(), *mIndex); mCurrentCategory->second.addSetting(setting); @@ -535,18 +476,13 @@ CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum( } CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( - const std::string& key, const std::string& label, QColor default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_.name().toUtf8().data()); - - default_.setNamedColor( - QString::fromUtf8(Settings::Manager::getString(key, mCurrentCategory->second.getKey()).c_str())); - CSMPrefs::ColourSetting* setting - = new CSMPrefs::ColourSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::ColourSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); @@ -554,39 +490,32 @@ CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( } CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut( - const std::string& key, const std::string& label, const QKeySequence& default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - std::string seqStr = getShortcutManager().convertToString(default_); - setDefault(key, seqStr); - // Setup with actual data QKeySequence sequence; - getShortcutManager().convertFromString( - Settings::Manager::getString(key, mCurrentCategory->second.getKey()), sequence); - getShortcutManager().setSequence(key, sequence); + getShortcutManager().convertFromString(value, sequence); + getShortcutManager().setSequence(value.mName, sequence); - CSMPrefs::ShortcutSetting* setting = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, key, label); + CSMPrefs::ShortcutSetting* setting + = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::StringSetting& CSMPrefs::State::declareString( - const std::string& key, const std::string& label, std::string default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - setDefault(key, default_); - - default_ = Settings::Manager::getString(key, mCurrentCategory->second.getKey()); - CSMPrefs::StringSetting* setting - = new CSMPrefs::StringSetting(&mCurrentCategory->second, &mMutex, key, label, default_); + = new CSMPrefs::StringSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); @@ -594,55 +523,31 @@ CSMPrefs::StringSetting& CSMPrefs::State::declareString( } CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier( - const std::string& key, const std::string& label, int default_) + Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - std::string modStr = getShortcutManager().convertToString(default_); - setDefault(key, modStr); - // Setup with actual data int modifier; - getShortcutManager().convertFromString( - Settings::Manager::getString(key, mCurrentCategory->second.getKey()), modifier); - getShortcutManager().setModifier(key, modifier); + getShortcutManager().convertFromString(value.get(), modifier); + getShortcutManager().setModifier(value.mName, modifier); - CSMPrefs::ModifierSetting* setting = new CSMPrefs::ModifierSetting(&mCurrentCategory->second, &mMutex, key, label); + CSMPrefs::ModifierSetting* setting + = new CSMPrefs::ModifierSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } -void CSMPrefs::State::declareSeparator() +void CSMPrefs::State::declareSubcategory(const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); - CSMPrefs::Setting* setting = new CSMPrefs::Setting(&mCurrentCategory->second, &mMutex, "", ""); - - mCurrentCategory->second.addSetting(setting); -} - -void CSMPrefs::State::declareSubcategory(const std::string& label) -{ - if (mCurrentCategory == mCategories.end()) - throw std::logic_error("no category for setting"); - - CSMPrefs::Setting* setting = new CSMPrefs::Setting(&mCurrentCategory->second, &mMutex, "", label); - - mCurrentCategory->second.addSetting(setting); -} - -void CSMPrefs::State::setDefault(const std::string& key, const std::string& default_) -{ - Settings::CategorySetting fullKey(mCurrentCategory->second.getKey(), key); - - Settings::CategorySettingValueMap::iterator iter = Settings::Manager::mDefaultSettings.find(fullKey); - - if (iter == Settings::Manager::mDefaultSettings.end()) - Settings::Manager::mDefaultSettings.insert(std::make_pair(fullKey, default_)); + mCurrentCategory->second.addSubcategory( + new CSMPrefs::Subcategory(&mCurrentCategory->second, &mMutex, label, *mIndex)); } CSMPrefs::State::State(const Files::ConfigurationManager& configurationManager) @@ -650,6 +555,8 @@ CSMPrefs::State::State(const Files::ConfigurationManager& configurationManager) , mDefaultConfigFile("defaults-cs.bin") , mConfigurationManager(configurationManager) , mCurrentCategory(mCategories.end()) + , mIndex(std::make_unique()) + , mValues(std::make_unique(*mIndex)) { if (sThis) throw std::logic_error("An instance of CSMPRefs::State already exists"); @@ -709,27 +616,13 @@ CSMPrefs::State& CSMPrefs::State::get() void CSMPrefs::State::resetCategory(const std::string& category) { - for (Settings::CategorySettingValueMap::iterator i = Settings::Manager::mUserSettings.begin(); - i != Settings::Manager::mUserSettings.end(); ++i) - { - // if the category matches - if (i->first.first == category) - { - // mark the setting as changed - Settings::Manager::mChangedSettings.insert(std::make_pair(i->first.first, i->first.second)); - // reset the value to the default - i->second = Settings::Manager::mDefaultSettings[i->first]; - } - } - Collection::iterator container = mCategories.find(category); if (container != mCategories.end()) { - Category settings = container->second; - for (Category::Iterator i = settings.begin(); i != settings.end(); ++i) + for (Setting* setting : container->second) { - (*i)->updateWidget(); - update(**i); + setting->reset(); + update(*setting); } } } diff --git a/apps/opencs/model/prefs/state.hpp b/apps/opencs/model/prefs/state.hpp index 354f4552e3..821322d586 100644 --- a/apps/opencs/model/prefs/state.hpp +++ b/apps/opencs/model/prefs/state.hpp @@ -17,6 +17,11 @@ class QColor; +namespace Settings +{ + class Index; +} + namespace CSMPrefs { class IntSetting; @@ -27,6 +32,8 @@ namespace CSMPrefs class ModifierSetting; class Setting; class StringSetting; + class EnumSettingValue; + struct Values; /// \brief User settings state /// @@ -50,43 +57,40 @@ namespace CSMPrefs Collection mCategories; Iterator mCurrentCategory; QMutex mMutex; + std::unique_ptr mIndex; + std::unique_ptr mValues; - // not implemented - State(const State&); - State& operator=(const State&); - - private: void declare(); void declareCategory(const std::string& key); - IntSetting& declareInt(const std::string& key, const std::string& label, int default_); - DoubleSetting& declareDouble(const std::string& key, const std::string& label, double default_); + IntSetting& declareInt(Settings::SettingValue& value, const QString& label); - BoolSetting& declareBool(const std::string& key, const std::string& label, bool default_); + DoubleSetting& declareDouble(Settings::SettingValue& value, const QString& label); - EnumSetting& declareEnum(const std::string& key, const std::string& label, EnumValue default_); + BoolSetting& declareBool(Settings::SettingValue& value, const QString& label); - ColourSetting& declareColour(const std::string& key, const std::string& label, QColor default_); + EnumSetting& declareEnum(EnumSettingValue& value, const QString& label); - ShortcutSetting& declareShortcut( - const std::string& key, const std::string& label, const QKeySequence& default_); + ColourSetting& declareColour(Settings::SettingValue& value, const QString& label); - StringSetting& declareString(const std::string& key, const std::string& label, std::string default_); + ShortcutSetting& declareShortcut(Settings::SettingValue& value, const QString& label); - ModifierSetting& declareModifier(const std::string& key, const std::string& label, int modifier_); + StringSetting& declareString(Settings::SettingValue& value, const QString& label); - void declareSeparator(); + ModifierSetting& declareModifier(Settings::SettingValue& value, const QString& label); - void declareSubcategory(const std::string& label); - - void setDefault(const std::string& key, const std::string& default_); + void declareSubcategory(const QString& label); public: State(const Files::ConfigurationManager& configurationManager); + State(const State&) = delete; + ~State(); + State& operator=(const State&) = delete; + void save(); Iterator begin(); diff --git a/apps/opencs/model/prefs/stringsetting.cpp b/apps/opencs/model/prefs/stringsetting.cpp index 2a8fdd587a..4fa2955840 100644 --- a/apps/opencs/model/prefs/stringsetting.cpp +++ b/apps/opencs/model/prefs/stringsetting.cpp @@ -1,6 +1,7 @@ #include "stringsetting.hpp" +#include #include #include @@ -12,9 +13,8 @@ #include "state.hpp" CSMPrefs::StringSetting::StringSetting( - Category* parent, QMutex* mutex, const std::string& key, const std::string& label, std::string_view default_) - : Setting(parent, mutex, key, label) - , mDefault(default_) + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mWidget(nullptr) { } @@ -25,35 +25,33 @@ CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip(const std::string& return *this; } -std::pair CSMPrefs::StringSetting::makeWidgets(QWidget* parent) +CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent) { - mWidget = new QLineEdit(QString::fromUtf8(mDefault.c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); + + mWidget = new QLineEdit(QString::fromStdString(getValue()), parent); + mWidget->setMinimumWidth(300); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8(mTooltip.c_str()); + label->setToolTip(tooltip); mWidget->setToolTip(tooltip); } connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged); - return std::make_pair(static_cast(nullptr), mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::StringSetting::updateWidget() { if (mWidget) - { - mWidget->setText(QString::fromStdString(Settings::Manager::getString(getKey(), getParent()->getKey()))); - } + mWidget->setText(QString::fromStdString(getValue())); } void CSMPrefs::StringSetting::textChanged(const QString& text) { - { - QMutexLocker lock(getMutex()); - Settings::Manager::setString(getKey(), getParent()->getKey(), text.toStdString()); - } - + setValue(text.toStdString()); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/stringsetting.hpp b/apps/opencs/model/prefs/stringsetting.hpp index 4b5499ef86..0a7d2a4935 100644 --- a/apps/opencs/model/prefs/stringsetting.hpp +++ b/apps/opencs/model/prefs/stringsetting.hpp @@ -14,22 +14,22 @@ class QWidget; namespace CSMPrefs { class Category; - class StringSetting : public Setting + + class StringSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; - std::string mDefault; QLineEdit* mWidget; public: - StringSetting(Category* parent, QMutex* mutex, const std::string& key, const std::string& label, - std::string_view default_); + explicit StringSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); StringSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. - std::pair makeWidgets(QWidget* parent) override; + SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; diff --git a/apps/opencs/model/prefs/subcategory.cpp b/apps/opencs/model/prefs/subcategory.cpp new file mode 100644 index 0000000000..815025daec --- /dev/null +++ b/apps/opencs/model/prefs/subcategory.cpp @@ -0,0 +1,18 @@ +#include "subcategory.hpp" + +#include + +namespace CSMPrefs +{ + class Category; + + Subcategory::Subcategory(Category* parent, QMutex* mutex, const QString& label, Settings::Index& index) + : Setting(parent, mutex, "", label, index) + { + } + + SettingWidgets Subcategory::makeWidgets(QWidget* /*parent*/) + { + return SettingWidgets{ .mLabel = nullptr, .mInput = nullptr }; + } +} diff --git a/apps/opencs/model/prefs/subcategory.hpp b/apps/opencs/model/prefs/subcategory.hpp new file mode 100644 index 0000000000..4c661ad0fa --- /dev/null +++ b/apps/opencs/model/prefs/subcategory.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_SUBCATEGORY_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_SUBCATEGORY_H + +#include "setting.hpp" + +#include +#include + +namespace CSMPrefs +{ + class Category; + + class Subcategory final : public Setting + { + Q_OBJECT + + public: + explicit Subcategory(Category* parent, QMutex* mutex, const QString& label, Settings::Index& index); + + SettingWidgets makeWidgets(QWidget* parent) override; + + void updateWidget() override {} + + void reset() override {} + }; +} + +#endif diff --git a/apps/opencs/model/prefs/values.hpp b/apps/opencs/model/prefs/values.hpp new file mode 100644 index 0000000000..1339fa62ed --- /dev/null +++ b/apps/opencs/model/prefs/values.hpp @@ -0,0 +1,548 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_VALUES_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_VALUES_H + +#include "enumvalueview.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace CSMPrefs +{ + class EnumSanitizer final : public Settings::Sanitizer + { + public: + explicit EnumSanitizer(std::span values) + : mValues(values) + { + } + + std::string apply(const std::string& value) const override + { + const auto hasValue = [&](const EnumValueView& v) { return v.mValue == value; }; + if (std::find_if(mValues.begin(), mValues.end(), hasValue) == mValues.end()) + { + std::ostringstream message; + message << "Invalid enum value: " << value; + throw std::runtime_error(message.str()); + } + return value; + } + + private: + std::span mValues; + }; + + inline std::unique_ptr> makeEnumSanitizerString( + std::span values) + { + return std::make_unique(values); + } + + class EnumSettingValue + { + public: + explicit EnumSettingValue(Settings::Index& index, std::string_view category, std::string_view name, + std::span values, std::size_t defaultValueIndex) + : mValue( + index, category, name, std::string(values[defaultValueIndex].mValue), makeEnumSanitizerString(values)) + , mEnumValues(values) + { + } + + Settings::SettingValue& getValue() { return mValue; } + + std::span getEnumValues() const { return mEnumValues; } + + private: + Settings::SettingValue mValue; + std::span mEnumValues; + }; + + struct WindowsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Windows"; + + static constexpr std::array sMainwindowScrollbarValues{ + EnumValueView{ + "Scrollbar Only", "Simple addition of scrollbars, the view window does not grow automatically." }, + EnumValueView{ "Grow Only", "The view window grows as subviews are added. No scrollbars." }, + EnumValueView{ + "Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further." }, + }; + + Settings::SettingValue mDefaultWidth{ mIndex, sName, "default-width", 800 }; + Settings::SettingValue mDefaultHeight{ mIndex, sName, "default-height", 600 }; + Settings::SettingValue mShowStatusbar{ mIndex, sName, "show-statusbar", true }; + Settings::SettingValue mReuse{ mIndex, sName, "reuse", true }; + Settings::SettingValue mMaxSubviews{ mIndex, sName, "max-subviews", 256 }; + Settings::SettingValue mHideSubview{ mIndex, sName, "hide-subview", false }; + Settings::SettingValue mMinimumWidth{ mIndex, sName, "minimum-width", 325 }; + EnumSettingValue mMainwindowScrollbar{ mIndex, sName, "mainwindow-scrollbar", sMainwindowScrollbarValues, 0 }; + Settings::SettingValue mGrowLimit{ mIndex, sName, "grow-limit", false }; + }; + + struct RecordsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Records"; + + static constexpr std::array sRecordValues{ + EnumValueView{ "Icon and Text", "" }, + EnumValueView{ "Icon Only", "" }, + EnumValueView{ "Text Only", "" }, + }; + + EnumSettingValue mStatusFormat{ mIndex, sName, "status-format", sRecordValues, 0 }; + EnumSettingValue mTypeFormat{ mIndex, sName, "type-format", sRecordValues, 0 }; + }; + + struct IdTablesCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "ID Tables"; + + static constexpr std::array sDoubleClickValues{ + EnumValueView{ "Edit in Place", "Edit the clicked cell" }, + EnumValueView{ "Edit Record", "Open a dialogue subview for the clicked record" }, + EnumValueView{ "View", "Open a scene subview for the clicked record (not available everywhere)" }, + EnumValueView{ "Revert", "" }, + EnumValueView{ "Delete", "" }, + EnumValueView{ "Edit Record and Close", "" }, + EnumValueView{ + "View and Close", "Open a scene subview for the clicked record and close the table subview" }, + }; + + static constexpr std::array sJumpAndSelectValues{ + EnumValueView{ "Jump and Select", "Scroll new record into view and make it the selection" }, + EnumValueView{ "Jump Only", "Scroll new record into view" }, + EnumValueView{ "No Jump", "No special action" }, + }; + + EnumSettingValue mDouble{ mIndex, sName, "double", sDoubleClickValues, 0 }; + EnumSettingValue mDoubleS{ mIndex, sName, "double-s", sDoubleClickValues, 1 }; + EnumSettingValue mDoubleC{ mIndex, sName, "double-c", sDoubleClickValues, 2 }; + EnumSettingValue mDoubleSc{ mIndex, sName, "double-sc", sDoubleClickValues, 5 }; + EnumSettingValue mJumpToAdded{ mIndex, sName, "jump-to-added", sJumpAndSelectValues, 0 }; + Settings::SettingValue mExtendedConfig{ mIndex, sName, "extended-config", false }; + Settings::SettingValue mSubviewNewWindow{ mIndex, sName, "subview-new-window", false }; + Settings::SettingValue mFilterDelay{ mIndex, sName, "filter-delay", 500 }; + }; + + struct IdDialoguesCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "ID Dialogues"; + + Settings::SettingValue mToolbar{ mIndex, sName, "toolbar", true }; + }; + + struct ReportsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Reports"; + + static constexpr std::array sReportValues{ + EnumValueView{ "None", "" }, + EnumValueView{ "Edit", "Open a table or dialogue suitable for addressing the listed report" }, + EnumValueView{ "Remove", "Remove the report from the report table" }, + EnumValueView{ "Edit And Remove", + "Open a table or dialogue suitable for addressing the listed report, then remove the report from the " + "report table" }, + }; + + EnumSettingValue mDouble{ mIndex, sName, "double", sReportValues, 1 }; + EnumSettingValue mDoubleS{ mIndex, sName, "double-s", sReportValues, 2 }; + EnumSettingValue mDoubleC{ mIndex, sName, "double-c", sReportValues, 3 }; + EnumSettingValue mDoubleSc{ mIndex, sName, "double-sc", sReportValues, 0 }; + Settings::SettingValue mIgnoreBaseRecords{ mIndex, sName, "ignore-base-records", false }; + }; + + struct SearchAndReplaceCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Search & Replace"; + + Settings::SettingValue mCharBefore{ mIndex, sName, "char-before", 10 }; + Settings::SettingValue mCharAfter{ mIndex, sName, "char-after", 10 }; + Settings::SettingValue mAutoDelete{ mIndex, sName, "auto-delete", true }; + }; + + struct ScriptsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Scripts"; + + static constexpr std::array sWarningValues{ + EnumValueView{ "Ignore", "Do not report warning" }, + EnumValueView{ "Normal", "Report warnings as warning" }, + EnumValueView{ "Strict", "Promote warning to an error" }, + }; + + Settings::SettingValue mShowLinenum{ mIndex, sName, "show-linenum", true }; + Settings::SettingValue mWrapLines{ mIndex, sName, "wrap-lines", false }; + Settings::SettingValue mMonoFont{ mIndex, sName, "mono-font", true }; + Settings::SettingValue mTabWidth{ mIndex, sName, "tab-width", 4 }; + EnumSettingValue mWarnings{ mIndex, sName, "warnings", sWarningValues, 1 }; + Settings::SettingValue mToolbar{ mIndex, sName, "toolbar", true }; + Settings::SettingValue mCompileDelay{ mIndex, sName, "compile-delay", 100 }; + Settings::SettingValue mErrorHeight{ mIndex, sName, "error-height", 100 }; + Settings::SettingValue mHighlightOccurrences{ mIndex, sName, "highlight-occurrences", true }; + Settings::SettingValue mColourHighlight{ mIndex, sName, "colour-highlight", "lightcyan" }; + Settings::SettingValue mColourInt{ mIndex, sName, "colour-int", "#aa55ff" }; + Settings::SettingValue mColourFloat{ mIndex, sName, "colour-float", "magenta" }; + Settings::SettingValue mColourName{ mIndex, sName, "colour-name", "grey" }; + Settings::SettingValue mColourKeyword{ mIndex, sName, "colour-keyword", "red" }; + Settings::SettingValue mColourSpecial{ mIndex, sName, "colour-special", "darkorange" }; + Settings::SettingValue mColourComment{ mIndex, sName, "colour-comment", "green" }; + Settings::SettingValue mColourId{ mIndex, sName, "colour-id", "#0055ff" }; + }; + + struct GeneralInputCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "General Input"; + + Settings::SettingValue mCycle{ mIndex, sName, "cycle", false }; + }; + + struct SceneInputCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "3D Scene Input"; + + Settings::SettingValue mNaviWheelFactor{ mIndex, sName, "navi-wheel-factor", 8 }; + Settings::SettingValue mSNaviSensitivity{ mIndex, sName, "s-navi-sensitivity", 50 }; + Settings::SettingValue mPNaviFreeSensitivity{ mIndex, sName, "p-navi-free-sensitivity", 1 / 650.0 }; + Settings::SettingValue mPNaviFreeInvert{ mIndex, sName, "p-navi-free-invert", false }; + Settings::SettingValue mNaviFreeLinSpeed{ mIndex, sName, "navi-free-lin-speed", 1000 }; + Settings::SettingValue mNaviFreeRotSpeed{ mIndex, sName, "navi-free-rot-speed", 3.14 / 2 }; + Settings::SettingValue mNaviFreeSpeedMult{ mIndex, sName, "navi-free-speed-mult", 8 }; + Settings::SettingValue mPNaviOrbitSensitivity{ mIndex, sName, "p-navi-orbit-sensitivity", 1 / 650.0 }; + Settings::SettingValue mPNaviOrbitInvert{ mIndex, sName, "p-navi-orbit-invert", false }; + Settings::SettingValue mNaviOrbitRotSpeed{ mIndex, sName, "navi-orbit-rot-speed", 3.14 / 4 }; + Settings::SettingValue mNaviOrbitSpeedMult{ mIndex, sName, "navi-orbit-speed-mult", 4 }; + Settings::SettingValue mNaviOrbitConstRoll{ mIndex, sName, "navi-orbit-const-roll", true }; + Settings::SettingValue mContextSelect{ mIndex, sName, "context-select", false }; + Settings::SettingValue mDragFactor{ mIndex, sName, "drag-factor", 1 }; + Settings::SettingValue mDragWheelFactor{ mIndex, sName, "drag-wheel-factor", 1 }; + Settings::SettingValue mDragShiftFactor{ mIndex, sName, "drag-shift-factor", 4 }; + Settings::SettingValue mRotateFactor{ mIndex, sName, "rotate-factor", 0.007 }; + }; + + struct RenderingCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Rendering"; + + Settings::SettingValue mFramerateLimit{ mIndex, sName, "framerate-limit", 60 }; + Settings::SettingValue mCameraFov{ mIndex, sName, "camera-fov", 90 }; + Settings::SettingValue mCameraOrtho{ mIndex, sName, "camera-ortho", false }; + Settings::SettingValue mCameraOrthoSize{ mIndex, sName, "camera-ortho-size", 100 }; + Settings::SettingValue mObjectMarkerAlpha{ mIndex, sName, "object-marker-alpha", 0.5 }; + Settings::SettingValue mSceneUseGradient{ mIndex, sName, "scene-use-gradient", true }; + Settings::SettingValue mSceneDayBackgroundColour{ mIndex, sName, "scene-day-background-colour", + "#6e7880" }; + Settings::SettingValue mSceneDayGradientColour{ mIndex, sName, "scene-day-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneBrightBackgroundColour{ mIndex, sName, + "scene-bright-background-colour", "#4f575c" }; + Settings::SettingValue mSceneBrightGradientColour{ mIndex, sName, "scene-bright-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneNightBackgroundColour{ mIndex, sName, "scene-night-background-colour", + "#404d4f" }; + Settings::SettingValue mSceneNightGradientColour{ mIndex, sName, "scene-night-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneDayNightSwitchNodes{ mIndex, sName, "scene-day-night-switch-nodes", true }; + }; + + struct TooltipsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Tooltips"; + + Settings::SettingValue mScene{ mIndex, sName, "scene", true }; + Settings::SettingValue mSceneHideBasic{ mIndex, sName, "scene-hide-basic", false }; + Settings::SettingValue mSceneDelay{ mIndex, sName, "scene-delay", 500 }; + }; + + struct SceneEditingCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "3D Scene Editing"; + + static constexpr std::array sInsertOutsideCellValues{ + EnumValueView{ "Create cell and insert", "" }, + EnumValueView{ "Discard", "" }, + EnumValueView{ "Insert anyway", "" }, + }; + + static constexpr std::array sInsertOutsideVisibleCellValues{ + EnumValueView{ "Show cell and insert", "" }, + EnumValueView{ "Discard", "" }, + EnumValueView{ "Insert anyway", "" }, + }; + + static constexpr std::array sLandEditOutsideCellValues{ + EnumValueView{ "Create cell and land, then edit", "" }, + EnumValueView{ "Discard", "" }, + }; + + static constexpr std::array sLandEditOutsideVisibleCellValues{ + EnumValueView{ "Show cell and edit", "" }, + EnumValueView{ "Discard", "" }, + }; + + static constexpr std::array sSelectAction{ + EnumValueView{ "Select only", "" }, + EnumValueView{ "Add to selection", "" }, + EnumValueView{ "Remove from selection", "" }, + EnumValueView{ "Invert selection", "" }, + }; + + Settings::SettingValue mGridsnapMovement{ mIndex, sName, "gridsnap-movement", 16 }; + Settings::SettingValue mGridsnapRotation{ mIndex, sName, "gridsnap-rotation", 15 }; + Settings::SettingValue mGridsnapScale{ mIndex, sName, "gridsnap-scale", 0.25 }; + Settings::SettingValue mDistance{ mIndex, sName, "distance", 50 }; + EnumSettingValue mOutsideDrop{ mIndex, sName, "outside-drop", sInsertOutsideCellValues, 0 }; + EnumSettingValue mOutsideVisibleDrop{ mIndex, sName, "outside-visible-drop", sInsertOutsideVisibleCellValues, + 0 }; + EnumSettingValue mOutsideLandedit{ mIndex, sName, "outside-landedit", sLandEditOutsideCellValues, 0 }; + EnumSettingValue mOutsideVisibleLandedit{ mIndex, sName, "outside-visible-landedit", + sLandEditOutsideVisibleCellValues, 0 }; + Settings::SettingValue mTexturebrushMaximumsize{ mIndex, sName, "texturebrush-maximumsize", 50 }; + Settings::SettingValue mShapebrushMaximumsize{ mIndex, sName, "shapebrush-maximumsize", 100 }; + Settings::SettingValue mLandeditPostSmoothpainting{ mIndex, sName, "landedit-post-smoothpainting", + false }; + Settings::SettingValue mLandeditPostSmoothstrength{ mIndex, sName, "landedit-post-smoothstrength", + 0.25 }; + Settings::SettingValue mOpenListView{ mIndex, sName, "open-list-view", false }; + EnumSettingValue mPrimarySelectAction{ mIndex, sName, "primary-select-action", sSelectAction, 0 }; + EnumSettingValue mSecondarySelectAction{ mIndex, sName, "secondary-select-action", sSelectAction, 1 }; + }; + + struct KeyBindingsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Key Bindings"; + + Settings::SettingValue mDocumentFileNewgame{ mIndex, sName, "document-file-newgame", "Ctrl+N" }; + Settings::SettingValue mDocumentFileNewaddon{ mIndex, sName, "document-file-newaddon", "" }; + Settings::SettingValue mDocumentFileOpen{ mIndex, sName, "document-file-open", "Ctrl+O" }; + Settings::SettingValue mDocumentFileSave{ mIndex, sName, "document-file-save", "Ctrl+S" }; + Settings::SettingValue mDocumentHelpHelp{ mIndex, sName, "document-help-help", "F1" }; + Settings::SettingValue mDocumentHelpTutorial{ mIndex, sName, "document-help-tutorial", "" }; + Settings::SettingValue mDocumentFileVerify{ mIndex, sName, "document-file-verify", "" }; + Settings::SettingValue mDocumentFileMerge{ mIndex, sName, "document-file-merge", "" }; + Settings::SettingValue mDocumentFileErrorlog{ mIndex, sName, "document-file-errorlog", "" }; + Settings::SettingValue mDocumentFileMetadata{ mIndex, sName, "document-file-metadata", "" }; + Settings::SettingValue mDocumentFileClose{ mIndex, sName, "document-file-close", "Ctrl+W" }; + Settings::SettingValue mDocumentFileExit{ mIndex, sName, "document-file-exit", "Ctrl+Q" }; + Settings::SettingValue mDocumentEditUndo{ mIndex, sName, "document-edit-undo", "Ctrl+Z" }; + Settings::SettingValue mDocumentEditRedo{ mIndex, sName, "document-edit-redo", "Ctrl+Shift+Z" }; + Settings::SettingValue mDocumentEditPreferences{ mIndex, sName, "document-edit-preferences", "" }; + Settings::SettingValue mDocumentEditSearch{ mIndex, sName, "document-edit-search", "Ctrl+F" }; + Settings::SettingValue mDocumentViewNewview{ mIndex, sName, "document-view-newview", "" }; + Settings::SettingValue mDocumentViewStatusbar{ mIndex, sName, "document-view-statusbar", "" }; + Settings::SettingValue mDocumentViewFilters{ mIndex, sName, "document-view-filters", "" }; + Settings::SettingValue mDocumentWorldRegions{ mIndex, sName, "document-world-regions", "" }; + Settings::SettingValue mDocumentWorldCells{ mIndex, sName, "document-world-cells", "" }; + Settings::SettingValue mDocumentWorldReferencables{ mIndex, sName, "document-world-referencables", + "" }; + Settings::SettingValue mDocumentWorldReferences{ mIndex, sName, "document-world-references", "" }; + Settings::SettingValue mDocumentWorldLands{ mIndex, sName, "document-world-lands", "" }; + Settings::SettingValue mDocumentWorldLandtextures{ mIndex, sName, "document-world-landtextures", + "" }; + Settings::SettingValue mDocumentWorldPathgrid{ mIndex, sName, "document-world-pathgrid", "" }; + Settings::SettingValue mDocumentWorldRegionmap{ mIndex, sName, "document-world-regionmap", "" }; + Settings::SettingValue mDocumentMechanicsGlobals{ mIndex, sName, "document-mechanics-globals", + "" }; + Settings::SettingValue mDocumentMechanicsGamesettings{ mIndex, sName, + "document-mechanics-gamesettings", "" }; + Settings::SettingValue mDocumentMechanicsScripts{ mIndex, sName, "document-mechanics-scripts", + "" }; + Settings::SettingValue mDocumentMechanicsSpells{ mIndex, sName, "document-mechanics-spells", "" }; + Settings::SettingValue mDocumentMechanicsEnchantments{ mIndex, sName, + "document-mechanics-enchantments", "" }; + Settings::SettingValue mDocumentMechanicsMagiceffects{ mIndex, sName, + "document-mechanics-magiceffects", "" }; + Settings::SettingValue mDocumentMechanicsStartscripts{ mIndex, sName, + "document-mechanics-startscripts", "" }; + Settings::SettingValue mDocumentCharacterSkills{ mIndex, sName, "document-character-skills", "" }; + Settings::SettingValue mDocumentCharacterClasses{ mIndex, sName, "document-character-classes", + "" }; + Settings::SettingValue mDocumentCharacterFactions{ mIndex, sName, "document-character-factions", + "" }; + Settings::SettingValue mDocumentCharacterRaces{ mIndex, sName, "document-character-races", "" }; + Settings::SettingValue mDocumentCharacterBirthsigns{ mIndex, sName, + "document-character-birthsigns", "" }; + Settings::SettingValue mDocumentCharacterTopics{ mIndex, sName, "document-character-topics", "" }; + Settings::SettingValue mDocumentCharacterJournals{ mIndex, sName, "document-character-journals", + "" }; + Settings::SettingValue mDocumentCharacterTopicinfos{ mIndex, sName, + "document-character-topicinfos", "" }; + Settings::SettingValue mDocumentCharacterJournalinfos{ mIndex, sName, + "document-character-journalinfos", "" }; + Settings::SettingValue mDocumentCharacterBodyparts{ mIndex, sName, "document-character-bodyparts", + "" }; + Settings::SettingValue mDocumentAssetsReload{ mIndex, sName, "document-assets-reload", "F5" }; + Settings::SettingValue mDocumentAssetsSounds{ mIndex, sName, "document-assets-sounds", "" }; + Settings::SettingValue mDocumentAssetsSoundgens{ mIndex, sName, "document-assets-soundgens", "" }; + Settings::SettingValue mDocumentAssetsMeshes{ mIndex, sName, "document-assets-meshes", "" }; + Settings::SettingValue mDocumentAssetsIcons{ mIndex, sName, "document-assets-icons", "" }; + Settings::SettingValue mDocumentAssetsMusic{ mIndex, sName, "document-assets-music", "" }; + Settings::SettingValue mDocumentAssetsSoundres{ mIndex, sName, "document-assets-soundres", "" }; + Settings::SettingValue mDocumentAssetsTextures{ mIndex, sName, "document-assets-textures", "" }; + Settings::SettingValue mDocumentAssetsVideos{ mIndex, sName, "document-assets-videos", "" }; + Settings::SettingValue mDocumentDebugRun{ mIndex, sName, "document-debug-run", "" }; + Settings::SettingValue mDocumentDebugShutdown{ mIndex, sName, "document-debug-shutdown", "" }; + Settings::SettingValue mDocumentDebugProfiles{ mIndex, sName, "document-debug-profiles", "" }; + Settings::SettingValue mDocumentDebugRunlog{ mIndex, sName, "document-debug-runlog", "" }; + Settings::SettingValue mTableEdit{ mIndex, sName, "table-edit", "" }; + Settings::SettingValue mTableAdd{ mIndex, sName, "table-add", "Shift+A" }; + Settings::SettingValue mTableClone{ mIndex, sName, "table-clone", "Shift+D" }; + Settings::SettingValue mTouchRecord{ mIndex, sName, "touch-record", "" }; + Settings::SettingValue mTableRevert{ mIndex, sName, "table-revert", "" }; + Settings::SettingValue mTableRemove{ mIndex, sName, "table-remove", "Delete" }; + Settings::SettingValue mTableMoveup{ mIndex, sName, "table-moveup", "" }; + Settings::SettingValue mTableMovedown{ mIndex, sName, "table-movedown", "" }; + Settings::SettingValue mTableView{ mIndex, sName, "table-view", "Shift+C" }; + Settings::SettingValue mTablePreview{ mIndex, sName, "table-preview", "Shift+V" }; + Settings::SettingValue mTableExtendeddelete{ mIndex, sName, "table-extendeddelete", "" }; + Settings::SettingValue mTableExtendedrevert{ mIndex, sName, "table-extendedrevert", "" }; + Settings::SettingValue mReporttableShow{ mIndex, sName, "reporttable-show", "" }; + Settings::SettingValue mReporttableRemove{ mIndex, sName, "reporttable-remove", "Delete" }; + Settings::SettingValue mReporttableReplace{ mIndex, sName, "reporttable-replace", "" }; + Settings::SettingValue mReporttableRefresh{ mIndex, sName, "reporttable-refresh", "" }; + Settings::SettingValue mSceneNaviPrimary{ mIndex, sName, "scene-navi-primary", "MMB" }; + Settings::SettingValue mSceneNaviSecondary{ mIndex, sName, "scene-navi-secondary", "Ctrl+MMB" }; + Settings::SettingValue mSceneOpenPrimary{ mIndex, sName, "scene-open-primary", "Shift+RMB" }; + Settings::SettingValue mSceneEditPrimary{ mIndex, sName, "scene-edit-primary", "RMB" }; + Settings::SettingValue mSceneEditSecondary{ mIndex, sName, "scene-edit-secondary", "Ctrl+RMB" }; + Settings::SettingValue mSceneSelectPrimary{ mIndex, sName, "scene-select-primary", "LMB" }; + Settings::SettingValue mSceneSelectSecondary{ mIndex, sName, "scene-select-secondary", + "Ctrl+LMB" }; + Settings::SettingValue mSceneSelectTertiary{ mIndex, sName, "scene-select-tertiary", "Shift+LMB" }; + Settings::SettingValue mSceneSpeedModifier{ mIndex, sName, "scene-speed-modifier", "Shift" }; + Settings::SettingValue mSceneDelete{ mIndex, sName, "scene-delete", "Delete" }; + Settings::SettingValue mSceneInstanceDrop{ mIndex, sName, "scene-instance-drop", "F" }; + Settings::SettingValue mSceneDuplicate{ mIndex, sName, "scene-duplicate", "Shift+C" }; + Settings::SettingValue mSceneLoadCamCell{ mIndex, sName, "scene-load-cam-cell", "Keypad+5" }; + Settings::SettingValue mSceneLoadCamEastcell{ mIndex, sName, "scene-load-cam-eastcell", + "Keypad+6" }; + Settings::SettingValue mSceneLoadCamNorthcell{ mIndex, sName, "scene-load-cam-northcell", + "Keypad+8" }; + Settings::SettingValue mSceneLoadCamWestcell{ mIndex, sName, "scene-load-cam-westcell", + "Keypad+4" }; + Settings::SettingValue mSceneLoadCamSouthcell{ mIndex, sName, "scene-load-cam-southcell", + "Keypad+2" }; + Settings::SettingValue mSceneEditAbort{ mIndex, sName, "scene-edit-abort", "Escape" }; + Settings::SettingValue mSceneFocusToolbar{ mIndex, sName, "scene-focus-toolbar", "T" }; + Settings::SettingValue mSceneRenderStats{ mIndex, sName, "scene-render-stats", "F3" }; + Settings::SettingValue mSceneClearSelection{ mIndex, sName, "scene-clear-selection", "Space" }; + Settings::SettingValue mSceneUnhideAll{ mIndex, sName, "scene-unhide-all", "Alt+H" }; + Settings::SettingValue mSceneToggleVisibility{ mIndex, sName, "scene-toggle-visibility", "H" }; + Settings::SettingValue mSceneGroup0{ mIndex, sName, "scene-group-0", "0" }; + Settings::SettingValue mSceneSave0{ mIndex, sName, "scene-save-0", "Ctrl+0" }; + Settings::SettingValue mSceneGroup1{ mIndex, sName, "scene-group-1", "1" }; + Settings::SettingValue mSceneSave1{ mIndex, sName, "scene-save-1", "Ctrl+1" }; + Settings::SettingValue mSceneGroup2{ mIndex, sName, "scene-group-2", "2" }; + Settings::SettingValue mSceneSave2{ mIndex, sName, "scene-save-2", "Ctrl+2" }; + Settings::SettingValue mSceneGroup3{ mIndex, sName, "scene-group-3", "3" }; + Settings::SettingValue mSceneSave3{ mIndex, sName, "scene-save-3", "Ctrl+3" }; + Settings::SettingValue mSceneGroup4{ mIndex, sName, "scene-group-4", "4" }; + Settings::SettingValue mSceneSave4{ mIndex, sName, "scene-save-4", "Ctrl+4" }; + Settings::SettingValue mSceneGroup5{ mIndex, sName, "scene-group-5", "5" }; + Settings::SettingValue mSceneSave5{ mIndex, sName, "scene-save-5", "Ctrl+5" }; + Settings::SettingValue mSceneGroup6{ mIndex, sName, "scene-group-6", "6" }; + Settings::SettingValue mSceneSave6{ mIndex, sName, "scene-save-6", "Ctrl+6" }; + Settings::SettingValue mSceneGroup7{ mIndex, sName, "scene-group-7", "7" }; + Settings::SettingValue mSceneSave7{ mIndex, sName, "scene-save-7", "Ctrl+7" }; + Settings::SettingValue mSceneGroup8{ mIndex, sName, "scene-group-8", "8" }; + Settings::SettingValue mSceneSave8{ mIndex, sName, "scene-save-8", "Ctrl+8" }; + Settings::SettingValue mSceneGroup9{ mIndex, sName, "scene-group-9", "9" }; + Settings::SettingValue mSceneSave9{ mIndex, sName, "scene-save-9", "Ctrl+9" }; + Settings::SettingValue mSceneAxisX{ mIndex, sName, "scene-axis-x", "X" }; + Settings::SettingValue mSceneAxisY{ mIndex, sName, "scene-axis-y", "Y" }; + Settings::SettingValue mSceneAxisZ{ mIndex, sName, "scene-axis-z", "Z" }; + Settings::SettingValue mSceneMoveSubmode{ mIndex, sName, "scene-submode-move", "G" }; + Settings::SettingValue mSceneScaleSubmode{ mIndex, sName, "scene-submode-scale", "V" }; + Settings::SettingValue mSceneRotateSubmode{ mIndex, sName, "scene-submode-rotate", "R" }; + Settings::SettingValue mSceneCameraCycle{ mIndex, sName, "scene-cam-cycle", "Tab" }; + Settings::SettingValue mSceneToggleMarkers{ mIndex, sName, "scene-toggle-markers", "F4" }; + Settings::SettingValue mFreeForward{ mIndex, sName, "free-forward", "W" }; + Settings::SettingValue mFreeBackward{ mIndex, sName, "free-backward", "S" }; + Settings::SettingValue mFreeLeft{ mIndex, sName, "free-left", "A" }; + Settings::SettingValue mFreeRight{ mIndex, sName, "free-right", "D" }; + Settings::SettingValue mFreeRollLeft{ mIndex, sName, "free-roll-left", "Q" }; + Settings::SettingValue mFreeRollRight{ mIndex, sName, "free-roll-right", "E" }; + Settings::SettingValue mFreeSpeedMode{ mIndex, sName, "free-speed-mode", "" }; + Settings::SettingValue mOrbitUp{ mIndex, sName, "orbit-up", "W" }; + Settings::SettingValue mOrbitDown{ mIndex, sName, "orbit-down", "S" }; + Settings::SettingValue mOrbitLeft{ mIndex, sName, "orbit-left", "A" }; + Settings::SettingValue mOrbitRight{ mIndex, sName, "orbit-right", "D" }; + Settings::SettingValue mOrbitRollLeft{ mIndex, sName, "orbit-roll-left", "Q" }; + Settings::SettingValue mOrbitRollRight{ mIndex, sName, "orbit-roll-right", "E" }; + Settings::SettingValue mOrbitSpeedMode{ mIndex, sName, "orbit-speed-mode", "" }; + Settings::SettingValue mOrbitCenterSelection{ mIndex, sName, "orbit-center-selection", "C" }; + Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", "" }; + Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", "" }; + }; + + struct ModelsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Models"; + + Settings::SettingValue mBaseanim{ mIndex, sName, "baseanim", "meshes/base_anim.nif" }; + Settings::SettingValue mBaseanimkna{ mIndex, sName, "baseanimkna", "meshes/base_animkna.nif" }; + Settings::SettingValue mBaseanimfemale{ mIndex, sName, "baseanimfemale", + "meshes/base_anim_female.nif" }; + Settings::SettingValue mWolfskin{ mIndex, sName, "wolfskin", "meshes/wolf/skin.nif" }; + }; + + struct Values : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + WindowsCategory mWindows{ mIndex }; + RecordsCategory mRecords{ mIndex }; + IdTablesCategory mIdTables{ mIndex }; + IdDialoguesCategory mIdDialogues{ mIndex }; + ReportsCategory mReports{ mIndex }; + SearchAndReplaceCategory mSearchAndReplace{ mIndex }; + ScriptsCategory mScripts{ mIndex }; + GeneralInputCategory mGeneralInput{ mIndex }; + SceneInputCategory mSceneInput{ mIndex }; + RenderingCategory mRendering{ mIndex }; + TooltipsCategory mTooltips{ mIndex }; + SceneEditingCategory mSceneEditing{ mIndex }; + KeyBindingsCategory mKeyBindings{ mIndex }; + ModelsCategory mModels{ mIndex }; + }; +} + +#endif diff --git a/apps/opencs/model/tools/effectlistcheck.cpp b/apps/opencs/model/tools/effectlistcheck.cpp new file mode 100644 index 0000000000..b8695bc419 --- /dev/null +++ b/apps/opencs/model/tools/effectlistcheck.cpp @@ -0,0 +1,88 @@ +#include "effectlistcheck.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace CSMTools +{ + void effectListCheck( + const std::vector& list, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id) + { + if (list.empty()) + { + messages.add(id, "No magic effects", "", CSMDoc::Message::Severity_Warning); + return; + } + + size_t i = 1; + for (const ESM::IndexedENAMstruct& effect : list) + { + const std::string number = std::to_string(i); + + // At the time of writing this effects, attributes and skills are mostly hardcoded + if (effect.mData.mEffectID < 0 || effect.mData.mEffectID >= ESM::MagicEffect::Length) + messages.add(id, "Effect #" + number + ": invalid effect ID", "", CSMDoc::Message::Severity_Error); + if (effect.mData.mSkill < -1 || effect.mData.mSkill >= ESM::Skill::Length) + messages.add(id, "Effect #" + number + ": invalid skill", "", CSMDoc::Message::Severity_Error); + if (effect.mData.mAttribute < -1 || effect.mData.mAttribute >= ESM::Attribute::Length) + messages.add(id, "Effect #" + number + ": invalid attribute", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mRange < ESM::RT_Self || effect.mData.mRange > ESM::RT_Target) + messages.add(id, "Effect #" + number + ": invalid range", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mArea < 0) + messages.add(id, "Effect #" + number + ": negative area", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mDuration < 0) + messages.add(id, "Effect #" + number + ": negative duration", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mMagnMin < 0) + messages.add( + id, "Effect #" + number + ": negative minimum magnitude", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mMagnMax < 0) + messages.add( + id, "Effect #" + number + ": negative maximum magnitude", "", CSMDoc::Message::Severity_Error); + else if (effect.mData.mMagnMax == 0) + messages.add( + id, "Effect #" + number + ": zero maximum magnitude", "", CSMDoc::Message::Severity_Warning); + + if (effect.mData.mMagnMin > effect.mData.mMagnMax) + messages.add(id, "Effect #" + number + ": minimum magnitude is higher than maximum magnitude", "", + CSMDoc::Message::Severity_Error); + + ++i; + } + } + + void ingredientEffectListCheck( + const ESM::Ingredient& ingredient, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id) + { + bool hasEffects = false; + + for (size_t i = 0; i < 4; i++) + { + if (ingredient.mData.mEffectID[i] == -1) + continue; + + hasEffects = true; + + const std::string number = std::to_string(i + 1); + if (ingredient.mData.mEffectID[i] < -1 || ingredient.mData.mEffectID[i] >= ESM::MagicEffect::Length) + messages.add(id, "Effect #" + number + ": invalid effect ID", "", CSMDoc::Message::Severity_Error); + if (ingredient.mData.mSkills[i] < -1 || ingredient.mData.mSkills[i] >= ESM::Skill::Length) + messages.add(id, "Effect #" + number + ": invalid skill", "", CSMDoc::Message::Severity_Error); + if (ingredient.mData.mAttributes[i] < -1 || ingredient.mData.mAttributes[i] >= ESM::Attribute::Length) + messages.add(id, "Effect #" + number + ": invalid attribute", "", CSMDoc::Message::Severity_Error); + } + + if (!hasEffects) + messages.add(id, "No magic effects", "", CSMDoc::Message::Severity_Warning); + } +} diff --git a/apps/opencs/model/tools/effectlistcheck.hpp b/apps/opencs/model/tools/effectlistcheck.hpp new file mode 100644 index 0000000000..832f3650eb --- /dev/null +++ b/apps/opencs/model/tools/effectlistcheck.hpp @@ -0,0 +1,30 @@ +#ifndef CSM_TOOLS_EFFECTLISTCHECK_H +#define CSM_TOOLS_EFFECTLISTCHECK_H + +#include + +namespace ESM +{ + struct IndexedENAMstruct; + struct Ingredient; +} + +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class UniversalId; +} + +namespace CSMTools +{ + void effectListCheck( + const std::vector& list, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id); + void ingredientEffectListCheck( + const ESM::Ingredient& ingredient, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id); +} + +#endif diff --git a/apps/opencs/model/tools/enchantmentcheck.cpp b/apps/opencs/model/tools/enchantmentcheck.cpp index d6cb22b738..8bd7d6cc4c 100644 --- a/apps/opencs/model/tools/enchantmentcheck.cpp +++ b/apps/opencs/model/tools/enchantmentcheck.cpp @@ -10,13 +10,14 @@ #include #include -#include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" +#include "effectlistcheck.hpp" + CSMTools::EnchantmentCheckStage::EnchantmentCheckStage(const CSMWorld::IdCollection& enchantments) : mEnchantments(enchantments) { @@ -54,47 +55,5 @@ void CSMTools::EnchantmentCheckStage::perform(int stage, CSMDoc::Messages& messa if (enchantment.mData.mCost > enchantment.mData.mCharge) messages.add(id, "Cost is higher than charge", "", CSMDoc::Message::Severity_Error); - if (enchantment.mEffects.mList.empty()) - { - messages.add(id, "Enchantment doesn't have any magic effects", "", CSMDoc::Message::Severity_Warning); - } - else - { - std::vector::const_iterator effect = enchantment.mEffects.mList.begin(); - - for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++) - { - const std::string number = std::to_string(i); - // At the time of writing this effects, attributes and skills are hardcoded - if (effect->mEffectID < 0 || effect->mEffectID > 142) - { - messages.add(id, "Effect #" + number + " is invalid", "", CSMDoc::Message::Severity_Error); - ++effect; - continue; - } - - if (effect->mSkill < -1 || effect->mSkill > 26) - messages.add( - id, "Effect #" + number + " affected skill is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mAttribute < -1 || effect->mAttribute > 7) - messages.add( - id, "Effect #" + number + " affected attribute is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mRange < 0 || effect->mRange > 2) - messages.add(id, "Effect #" + number + " range is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mArea < 0) - messages.add(id, "Effect #" + number + " area is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mDuration < 0) - messages.add(id, "Effect #" + number + " duration is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMin < 0) - messages.add( - id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMax < 0) - messages.add( - id, "Effect #" + number + " maximum magnitude is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMin > effect->mMagnMax) - messages.add(id, "Effect #" + number + " minimum magnitude is higher than maximum magnitude", "", - CSMDoc::Message::Severity_Error); - ++effect; - } - } + effectListCheck(enchantment.mEffects.mList, messages, id); } diff --git a/apps/opencs/model/tools/magiceffectcheck.cpp b/apps/opencs/model/tools/magiceffectcheck.cpp index a9ad4023fc..212b343e00 100644 --- a/apps/opencs/model/tools/magiceffectcheck.cpp +++ b/apps/opencs/model/tools/magiceffectcheck.cpp @@ -58,7 +58,12 @@ void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages& messa return; ESM::MagicEffect effect = record.get(); - CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, effect.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, CSMWorld::getRecordId(effect)); + + if (effect.mData.mSpeed <= 0.0f) + { + messages.add(id, "Speed is less than or equal to zero", "", CSMDoc::Message::Severity_Error); + } if (effect.mDescription.empty()) { diff --git a/apps/opencs/model/tools/mergestages.cpp b/apps/opencs/model/tools/mergestages.cpp index 5a7fa6c1b9..88cad267cb 100644 --- a/apps/opencs/model/tools/mergestages.cpp +++ b/apps/opencs/model/tools/mergestages.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -117,7 +116,7 @@ void CSMTools::MergeReferencesStage::perform(int stage, CSMDoc::Messages& messag ref.mOriginalCell = ref.mCell; ref.mRefNum.mIndex = mIndex[ref.mCell]++; - ref.mRefNum.mContentFile = 0; + ref.mRefNum.mContentFile = -1; ref.mNew = false; mState.mTarget->getData().getReferences().appendRecord(std::make_unique>( @@ -137,13 +136,12 @@ int CSMTools::PopulateLandTexturesMergeStage::setup() void CSMTools::PopulateLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mState.mSource.getData().getLandTextures().getRecord(stage); + const CSMWorld::Record& record = mState.mSource.getData().getLandTextures().getRecord(stage); if (!record.isDeleted()) { - mState.mTarget->getData().getLandTextures().appendRecord( - std::make_unique>(CSMWorld::Record( - CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); + mState.mTarget->getData().getLandTextures().appendRecord(std::make_unique>( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } @@ -189,9 +187,9 @@ void CSMTools::FixLandsAndLandTexturesMergeStage::perform(int stage, CSMDoc::Mes CSMWorld::IdTable& ltexTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); - const std::string& id = mState.mTarget->getData().getLand().getId(stage).getRefIdString(); + const auto& id = mState.mTarget->getData().getLand().getId(stage); - CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id); + CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id.getRefIdString()); cmd.redo(); // Get rid of base data diff --git a/apps/opencs/model/tools/pathgridcheck.cpp b/apps/opencs/model/tools/pathgridcheck.cpp index 6420c1c83c..f03b896321 100644 --- a/apps/opencs/model/tools/pathgridcheck.cpp +++ b/apps/opencs/model/tools/pathgridcheck.cpp @@ -44,9 +44,9 @@ void CSMTools::PathgridCheckStage::perform(int stage, CSMDoc::Messages& messages CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Pathgrid, pathgrid.mId); // check the number of pathgrid points - if (pathgrid.mData.mS2 < static_cast(pathgrid.mPoints.size())) + if (pathgrid.mData.mPoints < pathgrid.mPoints.size()) messages.add(id, "Less points than expected", "", CSMDoc::Message::Severity_Error); - else if (pathgrid.mData.mS2 > static_cast(pathgrid.mPoints.size())) + else if (pathgrid.mData.mPoints > pathgrid.mPoints.size()) messages.add(id, "More points than expected", "", CSMDoc::Message::Severity_Error); std::vector pointList(pathgrid.mPoints.size()); diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index 78f72f44c5..8f0df823c3 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -41,17 +41,17 @@ void CSMTools::RaceCheckStage::performPerRecord(int stage, CSMDoc::Messages& mes messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); // test for positive height - if (race.mData.mHeight.mMale <= 0) + if (race.mData.mMaleHeight <= 0) messages.add(id, "Male height is non-positive", "", CSMDoc::Message::Severity_Error); - if (race.mData.mHeight.mFemale <= 0) + if (race.mData.mFemaleHeight <= 0) messages.add(id, "Female height is non-positive", "", CSMDoc::Message::Severity_Error); // test for non-negative weight - if (race.mData.mWeight.mMale < 0) + if (race.mData.mMaleWeight < 0) messages.add(id, "Male weight is negative", "", CSMDoc::Message::Severity_Error); - if (race.mData.mWeight.mFemale < 0) + if (race.mData.mFemaleWeight < 0) messages.add(id, "Female weight is negative", "", CSMDoc::Message::Severity_Error); /// \todo check data members that can't be edited in the table view diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 6effa2cbf6..a00c3acd1c 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -38,6 +38,8 @@ #include "../world/record.hpp" #include "../world/universalid.hpp" +#include "effectlistcheck.hpp" + namespace ESM { class Script; @@ -330,7 +332,8 @@ void CSMTools::ReferenceableCheckStage::potionCheck( CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Potion, potion.mId); inventoryItemCheck(potion, messages, id.toString()); - /// \todo Check magic effects for validity + + effectListCheck(potion.mEffects.mList, messages, id); // Check that mentioned scripts exist scriptCheck(potion, messages, id.toString()); @@ -456,22 +459,12 @@ void CSMTools::ReferenceableCheckStage::creatureCheck( if (creature.mData.mLevel <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mStrength < 0) - messages.add(id, "Strength is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mIntelligence < 0) - messages.add(id, "Intelligence is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mWillpower < 0) - messages.add(id, "Willpower is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mAgility < 0) - messages.add(id, "Agility is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mSpeed < 0) - messages.add(id, "Speed is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mEndurance < 0) - messages.add(id, "Endurance is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mPersonality < 0) - messages.add(id, "Personality is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mLuck < 0) - messages.add(id, "Luck is negative", "", CSMDoc::Message::Severity_Warning); + for (size_t i = 0; i < creature.mData.mAttributes.size(); ++i) + { + if (creature.mData.mAttributes[i] < 0) + messages.add(id, ESM::Attribute::indexToRefId(i).toDebugString() + " is negative", {}, + CSMDoc::Message::Severity_Warning); + } if (creature.mData.mCombat < 0) messages.add(id, "Combat is negative", "", CSMDoc::Message::Severity_Warning); @@ -575,6 +568,8 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( // Check that mentioned scripts exist scriptCheck(ingredient, messages, id.toString()); + + ingredientEffectListCheck(ingredient, messages, id); } void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( @@ -701,25 +696,6 @@ void CSMTools::ReferenceableCheckStage::npcCheck( return; } } - else if (npc.mNpdt.mHealth != 0) - { - if (npc.mNpdt.mStrength == 0) - messages.add(id, "Strength is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mIntelligence == 0) - messages.add(id, "Intelligence is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mWillpower == 0) - messages.add(id, "Willpower is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mAgility == 0) - messages.add(id, "Agility is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mSpeed == 0) - messages.add(id, "Speed is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mEndurance == 0) - messages.add(id, "Endurance is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mPersonality == 0) - messages.add(id, "Personality is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mLuck == 0) - messages.add(id, "Luck is equal to zero", "", CSMDoc::Message::Severity_Warning); - } if (level <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp index 87d133a59e..306094f2f5 100644 --- a/apps/opencs/model/tools/referencecheck.cpp +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -18,16 +18,18 @@ #include #include +#include #include CSMTools::ReferenceCheckStage::ReferenceCheckStage(const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, const CSMWorld::IdCollection& cells, - const CSMWorld::IdCollection& factions) + const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& bodyparts) : mReferences(references) , mObjects(referencables) , mDataSet(referencables.getDataSet()) , mCells(cells) , mFactions(factions) + , mBodyParts(bodyparts) { mIgnoreBaseRecords = false; } @@ -43,15 +45,27 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages& message const CSMWorld::CellRef& cellRef = record.get(); const CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Reference, cellRef.mId); + // Check RefNum is unique per content file, otherwise can cause load issues + const auto refNum = cellRef.mRefNum; + const auto insertResult = mUsedReferenceIDs.emplace(refNum, cellRef.mId); + if (!insertResult.second) + messages.add(id, + "Duplicate RefNum: " + std::to_string(refNum.mContentFile) + std::string("-") + + std::to_string(refNum.mIndex) + " shared with cell reference " + + insertResult.first->second.toString(), + "", CSMDoc::Message::Severity_Error); + // Check reference id if (cellRef.mRefID.empty()) messages.add(id, "Instance is not based on an object", "", CSMDoc::Message::Severity_Error); else { // Check for non existing referenced object - if (mObjects.searchId(cellRef.mRefID) == -1) + if (mObjects.searchId(cellRef.mRefID) == -1 && mBodyParts.searchId(cellRef.mRefID) == -1) + { messages.add(id, "Instance of a non-existent object '" + cellRef.mRefID.getRefIdString() + "'", "", CSMDoc::Message::Severity_Error); + } else { // Check if reference charge is valid for it's proper referenced type @@ -98,14 +112,14 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages& message if (cellRef.mEnchantmentCharge < -1) messages.add(id, "Negative number of enchantment points", "", CSMDoc::Message::Severity_Error); - // Check if gold value isn't negative - if (cellRef.mGoldValue < 0) - messages.add(id, "Negative gold value", "", CSMDoc::Message::Severity_Error); + if (cellRef.mCount < 1) + messages.add(id, "Reference without count", {}, CSMDoc::Message::Severity_Error); } int CSMTools::ReferenceCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mUsedReferenceIDs.clear(); return mReferences.getSize(); } diff --git a/apps/opencs/model/tools/referencecheck.hpp b/apps/opencs/model/tools/referencecheck.hpp index 04cca2b803..58fb4e4170 100644 --- a/apps/opencs/model/tools/referencecheck.hpp +++ b/apps/opencs/model/tools/referencecheck.hpp @@ -8,6 +8,7 @@ namespace ESM { + struct BodyPart; struct Faction; } @@ -29,7 +30,8 @@ namespace CSMTools { public: ReferenceCheckStage(const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, - const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& factions); + const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& factions, + const CSMWorld::IdCollection& bodyparts); void perform(int stage, CSMDoc::Messages& messages) override; int setup() override; @@ -40,6 +42,8 @@ namespace CSMTools const CSMWorld::RefIdData& mDataSet; const CSMWorld::IdCollection& mCells; const CSMWorld::IdCollection& mFactions; + const CSMWorld::IdCollection& mBodyParts; + std::unordered_map mUsedReferenceIDs; bool mIgnoreBaseRecords; }; } diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 4affc1bd44..08e8a17714 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -47,9 +47,7 @@ void CSMTools::RegionCheckStage::perform(int stage, CSMDoc::Messages& messages) /// \todo test that the ID in mSleeplist exists // test that chances add up to 100 - int chances = region.mData.mClear + region.mData.mCloudy + region.mData.mFoggy + region.mData.mOvercast - + region.mData.mRain + region.mData.mThunder + region.mData.mAsh + region.mData.mBlight + region.mData.mSnow - + region.mData.mBlizzard; + auto chances = std::accumulate(region.mData.mProbabilities.begin(), region.mData.mProbabilities.end(), 0u); if (chances != 100) messages.add(id, "Weather chances do not add up to 100", "", CSMDoc::Message::Severity_Error); diff --git a/apps/opencs/model/tools/reportmodel.cpp b/apps/opencs/model/tools/reportmodel.cpp index 84a8c71f95..f9251acdab 100644 --- a/apps/opencs/model/tools/reportmodel.cpp +++ b/apps/opencs/model/tools/reportmodel.cpp @@ -171,7 +171,7 @@ void CSMTools::ReportModel::flagAsReplaced(int index) hint[0] = 'r'; - line.mHint = hint; + line.mHint = std::move(hint); emit dataChanged(this->index(index, 0), this->index(index, columnCount())); } diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index 7a76efa483..2ba8e33347 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -106,9 +106,6 @@ void CSMTools::ScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) mId = mDocument.getData().getScripts().getId(stage); - if (mDocument.isBlacklisted(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Script, mId.getRefIdString()))) - return; - // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index f91438dc22..07973bf08b 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -13,6 +13,8 @@ #include "../prefs/state.hpp" +#include "effectlistcheck.hpp" + CSMTools::SpellCheckStage::SpellCheckStage(const CSMWorld::IdCollection& spells) : mSpells(spells) { @@ -46,5 +48,5 @@ void CSMTools::SpellCheckStage::perform(int stage, CSMDoc::Messages& messages) if (spell.mData.mCost < 0) messages.add(id, "Spell cost is negative", "", CSMDoc::Message::Severity_Error); - /// \todo check data members that can't be edited in the table view + effectListCheck(spell.mEffects.mList, messages, id); } diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index e1e7a0dcd8..04548ca4cf 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -105,8 +105,8 @@ CSMDoc::OperationHolder* CSMTools::Tools::getVerifier() mData.getFactions(), mData.getScripts(), mData.getResources(CSMWorld::UniversalId::Type_Meshes), mData.getResources(CSMWorld::UniversalId::Type_Icons), mData.getBodyParts())); - mVerifierOperation->appendStage(new ReferenceCheckStage( - mData.getReferences(), mData.getReferenceables(), mData.getCells(), mData.getFactions())); + mVerifierOperation->appendStage(new ReferenceCheckStage(mData.getReferences(), mData.getReferenceables(), + mData.getCells(), mData.getFactions(), mData.getBodyParts())); mVerifierOperation->appendStage(new ScriptCheckStage(mDocument)); diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index ba99a33a28..fab90d951a 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -171,10 +171,9 @@ void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& message // Check info conditions - for (std::vector::const_iterator it = topicInfo.mSelects.begin(); - it != topicInfo.mSelects.end(); ++it) + for (const auto& select : topicInfo.mSelects) { - verifySelectStruct((*it), id, messages); + verifySelectStruct(select, id, messages); } } @@ -308,49 +307,15 @@ bool CSMTools::TopicInfoCheckStage::verifyItem( } bool CSMTools::TopicInfoCheckStage::verifySelectStruct( - const ESM::DialInfo::SelectStruct& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) + const ESM::DialogueCondition& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { CSMWorld::ConstInfoSelectWrapper infoCondition(select); - if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_None) + if (select.mFunction == ESM::DialogueCondition::Function_None) { messages.add(id, "Invalid condition '" + infoCondition.toString() + "'", "", CSMDoc::Message::Severity_Error); return false; } - else if (!infoCondition.variantTypeIsValid()) - { - std::ostringstream stream; - stream << "Value of condition '" << infoCondition.toString() << "' has invalid "; - - switch (select.mValue.getType()) - { - case ESM::VT_None: - stream << "None"; - break; - case ESM::VT_Short: - stream << "Short"; - break; - case ESM::VT_Int: - stream << "Int"; - break; - case ESM::VT_Long: - stream << "Long"; - break; - case ESM::VT_Float: - stream << "Float"; - break; - case ESM::VT_String: - stream << "String"; - break; - default: - stream << "unknown"; - break; - } - stream << " type"; - - messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); - return false; - } else if (infoCondition.conditionIsAlwaysTrue()) { messages.add( @@ -365,48 +330,48 @@ bool CSMTools::TopicInfoCheckStage::verifySelectStruct( } // Id checks - if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Global - && !verifyId(ESM::RefId::stringRefId(infoCondition.getVariableName()), mGlobals, id, messages)) + if (select.mFunction == ESM::DialogueCondition::Function_Global + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mGlobals, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Journal - && !verifyId(ESM::RefId::stringRefId(infoCondition.getVariableName()), mJournals, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_Journal + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mJournals, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Item - && !verifyItem(ESM::RefId::stringRefId(infoCondition.getVariableName()), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_Item + && !verifyItem(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Dead - && !verifyActor(ESM::RefId::stringRefId(infoCondition.getVariableName()), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_Dead + && !verifyActor(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotId - && !verifyActor(ESM::RefId::stringRefId(infoCondition.getVariableName()), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotId + && !verifyActor(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotFaction - && !verifyId(ESM::RefId::stringRefId(infoCondition.getVariableName()), mFactions, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotFaction + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mFactions, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotClass - && !verifyId(ESM::RefId::stringRefId(infoCondition.getVariableName()), mClasses, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotClass + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mClasses, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotRace - && !verifyId(ESM::RefId::stringRefId(infoCondition.getVariableName()), mRaces, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotRace + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mRaces, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotCell - && !verifyCell(infoCondition.getVariableName(), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotCell + && !verifyCell(select.mVariable, id, messages)) { return false; } diff --git a/apps/opencs/model/tools/topicinfocheck.hpp b/apps/opencs/model/tools/topicinfocheck.hpp index 1bb2fc61dc..3069fbc0ff 100644 --- a/apps/opencs/model/tools/topicinfocheck.hpp +++ b/apps/opencs/model/tools/topicinfocheck.hpp @@ -84,7 +84,7 @@ namespace CSMTools const ESM::RefId& name, int rank, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifyItem(const ESM::RefId& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifySelectStruct( - const ESM::DialInfo::SelectStruct& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + const ESM::DialogueCondition& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifySound(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); template diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 3a54bb3b0e..acbe6b5d38 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,7 @@ #include #include #include -#include +#include #include "data.hpp" @@ -66,6 +67,11 @@ namespace CSMWorld return mMaleParts[ESM::getMeshPart(index)]; } + const osg::Vec2f& ActorAdapter::RaceData::getGenderWeightHeight(bool isFemale) + { + return isFemale ? mWeightsHeights.mFemaleWeightHeight : mWeightsHeights.mMaleWeightHeight; + } + bool ActorAdapter::RaceData::hasDependency(const ESM::RefId& id) const { return mDependencies.find(id) != mDependencies.end(); @@ -89,10 +95,11 @@ namespace CSMWorld mDependencies.emplace(id); } - void ActorAdapter::RaceData::reset_data(const ESM::RefId& id, bool isBeast) + void ActorAdapter::RaceData::reset_data(const ESM::RefId& id, const WeightsHeights& raceStats, bool isBeast) { mId = id; mIsBeast = isBeast; + mWeightsHeights = raceStats; for (auto& str : mFemaleParts) str = ESM::RefId(); for (auto& str : mMaleParts) @@ -129,11 +136,14 @@ namespace CSMWorld if (mCreature || !mSkeletonOverride.empty()) return "meshes\\" + mSkeletonOverride; - bool firstPerson = false; bool beast = mRaceData ? mRaceData->isBeast() : false; - bool werewolf = false; - return SceneUtil::getActorSkeleton(firstPerson, mFemale, beast, werewolf); + if (beast) + return CSMPrefs::get()["Models"]["baseanimkna"].toString(); + else if (mFemale) + return CSMPrefs::get()["Models"]["baseanimfemale"].toString(); + else + return CSMPrefs::get()["Models"]["baseanim"].toString(); } ESM::RefId ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const @@ -159,6 +169,11 @@ namespace CSMWorld return it->second.first; } + const osg::Vec2f& ActorAdapter::ActorData::getRaceWeightHeight() const + { + return mRaceData->getGenderWeightHeight(isFemale()); + } + bool ActorAdapter::ActorData::hasDependency(const ESM::RefId& id) const { return mDependencies.find(id) != mDependencies.end(); @@ -464,13 +479,13 @@ namespace CSMWorld if (type == UniversalId::Type_Creature) { // Valid creature record - setupCreature(id, data); + setupCreature(id, std::move(data)); emit actorChanged(id); } else if (type == UniversalId::Type_Npc) { // Valid npc record - setupNpc(id, data); + setupNpc(id, std::move(data)); emit actorChanged(id); } else @@ -500,7 +515,11 @@ namespace CSMWorld } auto& race = raceRecord.get(); - data->reset_data(id, race.mData.mFlags & ESM::Race::Beast); + + WeightsHeights scaleStats = { osg::Vec2f(race.mData.mMaleWeight, race.mData.mMaleHeight), + osg::Vec2f(race.mData.mFemaleWeight, race.mData.mFemaleHeight) }; + + data->reset_data(id, scaleStats, race.mData.mFlags & ESM::Race::Beast); // Setup body parts for (int i = 0; i < mBodyParts.getSize(); ++i) @@ -661,7 +680,7 @@ namespace CSMWorld RaceDataPtr data = mCachedRaces.get(race); if (data) { - setupRace(race, data); + setupRace(race, std::move(data)); // Race was changed. Need to mark actor dependencies as dirty. // Cannot use markDirtyDependency because that would invalidate // the current iterator. @@ -679,7 +698,7 @@ namespace CSMWorld ActorDataPtr data = mCachedActors.get(actor); if (data) { - setupActor(actor, data); + setupActor(actor, std::move(data)); } } mDirtyActors.clear(); diff --git a/apps/opencs/model/world/actoradapter.hpp b/apps/opencs/model/world/actoradapter.hpp index 9747f448ae..d9a64cda31 100644 --- a/apps/opencs/model/world/actoradapter.hpp +++ b/apps/opencs/model/world/actoradapter.hpp @@ -41,6 +41,12 @@ namespace CSMWorld /// Tracks unique strings using RefIdSet = std::unordered_set; + struct WeightsHeights + { + osg::Vec2f mMaleWeightHeight; + osg::Vec2f mFemaleWeightHeight; + }; + /// Contains base race data shared between actors class RaceData { @@ -57,6 +63,8 @@ namespace CSMWorld const ESM::RefId& getFemalePart(ESM::PartReferenceType index) const; /// Retrieves the associated body part const ESM::RefId& getMalePart(ESM::PartReferenceType index) const; + + const osg::Vec2f& getGenderWeightHeight(bool isFemale); /// Checks if the race has a data dependency bool hasDependency(const ESM::RefId& id) const; @@ -67,7 +75,8 @@ namespace CSMWorld /// Marks an additional dependency void addOtherDependency(const ESM::RefId& id); /// Clears parts and dependencies - void reset_data(const ESM::RefId& raceId, bool isBeast = false); + void reset_data(const ESM::RefId& raceId, const WeightsHeights& raceStats = { { 1.f, 1.f }, { 1.f, 1.f } }, + bool isBeast = false); private: bool handles(ESM::PartReferenceType type) const; @@ -75,6 +84,7 @@ namespace CSMWorld bool mIsBeast; RacePartList mFemaleParts; RacePartList mMaleParts; + WeightsHeights mWeightsHeights; RefIdSet mDependencies; }; using RaceDataPtr = std::shared_ptr; @@ -96,6 +106,8 @@ namespace CSMWorld std::string getSkeleton() const; /// Retrieves the associated actor part ESM::RefId getPart(ESM::PartReferenceType index) const; + + const osg::Vec2f& getRaceWeightHeight() const; /// Checks if the actor has a data dependency bool hasDependency(const ESM::RefId& id) const; diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 9db6b3b042..fa42ee0f09 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -15,14 +15,15 @@ #include #include +#include #include #include #include "collectionbase.hpp" #include "columnbase.hpp" +#include "columnimp.hpp" #include "info.hpp" #include "land.hpp" -#include "landtexture.hpp" #include "record.hpp" #include "ref.hpp" @@ -72,19 +73,15 @@ namespace CSMWorld return ESM::RefId::stringRefId(Land::createUniqueRecordId(record.mX, record.mY)); } - inline void setRecordId(const ESM::RefId& id, LandTexture& record) + inline ESM::RefId getRecordId(const ESM::MagicEffect& record) { - int plugin = 0; - int index = 0; - - LandTexture::parseUniqueRecordId(id.getRefIdString(), plugin, index); - record.mPluginIndex = plugin; - record.mIndex = index; + return ESM::RefId::stringRefId(CSMWorld::getStringId(record.mId)); } - inline ESM::RefId getRecordId(const LandTexture& record) + inline void setRecordId(const ESM::RefId& id, ESM::MagicEffect& record) { - return ESM::RefId::stringRefId(LandTexture::createUniqueRecordId(record.mPluginIndex, record.mIndex)); + int index = ESM::MagicEffect::indexNameToIndex(id.getRefIdString()); + record.mId = ESM::RefId::index(ESM::REC_MGEF, static_cast(index)); } inline void setRecordId(const ESM::RefId& id, ESM::Skill& record) @@ -326,7 +323,7 @@ namespace CSMWorld const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { const int index = cloneRecordImp(origin, destination, type); - mRecords.at(index)->get().setPlugin(0); + mRecords.at(index)->get().setPlugin(-1); } template @@ -341,7 +338,7 @@ namespace CSMWorld const int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index)->get().setPlugin(0); + mRecords.at(index)->get().setPlugin(-1); return true; } @@ -504,7 +501,7 @@ namespace CSMWorld auto record2 = std::make_unique>(); record2->mState = Record::State_ModifiedOnly; - record2->mModified = record; + record2->mModified = std::move(record); insertRecord(std::move(record2), getAppendIndex(id, type), type); } diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index 981ec5278d..c31f9c01d0 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -2,11 +2,12 @@ #include #include +#include #include -#include #include #include +#include #include #include @@ -45,36 +46,13 @@ namespace CSMWorld }; } - /* LandTextureNicknameColumn */ - LandTextureNicknameColumn::LandTextureNicknameColumn() - : Column(Columns::ColumnId_TextureNickname, ColumnBase::Display_String) - { - } - - QVariant LandTextureNicknameColumn::get(const Record& record) const - { - return QString::fromStdString(record.get().mId.toString()); - } - - void LandTextureNicknameColumn::set(Record& record, const QVariant& data) - { - LandTexture copy = record.get(); - copy.mId = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified(copy); - } - - bool LandTextureNicknameColumn::isEditable() const - { - return true; - } - /* LandTextureIndexColumn */ LandTextureIndexColumn::LandTextureIndexColumn() - : Column(Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer) + : Column(Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer) { } - QVariant LandTextureIndexColumn::get(const Record& record) const + QVariant LandTextureIndexColumn::get(const Record& record) const { return record.get().mIndex; } @@ -100,22 +78,6 @@ namespace CSMWorld return false; } - /* LandTexturePluginIndexColumn */ - LandTexturePluginIndexColumn::LandTexturePluginIndexColumn() - : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) - { - } - - QVariant LandTexturePluginIndexColumn::get(const Record& record) const - { - return record.get().mPluginIndex; - } - - bool LandTexturePluginIndexColumn::isEditable() const - { - return false; - } - /* LandNormalsColumn */ LandNormalsColumn::LandNormalsColumn() : Column(Columns::ColumnId_LandNormalsIndex, ColumnBase::Display_String, 0) @@ -202,6 +164,8 @@ namespace CSMWorld copy.getLandData()->mHeights[i] = values[i]; } + copy.mFlags |= Land::Flag_HeightsNormals; + record.setModified(copy); } @@ -249,6 +213,8 @@ namespace CSMWorld copy.getLandData()->mColours[i] = values[i]; } + copy.mFlags |= Land::Flag_Colors; + record.setModified(copy); } @@ -296,6 +262,8 @@ namespace CSMWorld copy.getLandData()->mTextures[i] = values[i]; } + copy.mFlags |= Land::Flag_Textures; + record.setModified(copy); } @@ -316,7 +284,8 @@ namespace CSMWorld { return QString::fromUtf8(record.get().mRace.getRefIdString().c_str()); } - return QVariant(QVariant::UserType); + + return DisableTag::getVariant(); } void BodyPartRaceColumn::set(Record& record, const QVariant& data) @@ -333,6 +302,37 @@ namespace CSMWorld return true; } + SelectionGroupColumn::SelectionGroupColumn() + : Column(Columns::ColumnId_SelectionGroupObjects, ColumnBase::Display_None) + { + } + + QVariant SelectionGroupColumn::get(const Record& record) const + { + QVariant data; + QStringList selectionInfo; + const std::vector& instances = record.get().selectedInstances; + + for (const std::string& instance : instances) + selectionInfo << QString::fromStdString(instance); + data.setValue(selectionInfo); + + return data; + } + + void SelectionGroupColumn::set(Record& record, const QVariant& data) + { + ESM::SelectionGroup record2 = record.get(); + for (const auto& item : data.toStringList()) + record2.selectedInstances.push_back(item.toStdString()); + record.setModified(record2); + } + + bool SelectionGroupColumn::isEditable() const + { + return false; + } + std::optional getSkillIndex(std::string_view value) { int index = ESM::Skill::refIdToIndex(ESM::RefId::stringRefId(value)); diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 5e5ff83fcf..192f27ed46 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -14,8 +14,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -28,7 +30,6 @@ #include "columns.hpp" #include "info.hpp" #include "land.hpp" -#include "landtexture.hpp" #include "record.hpp" namespace CSMWorld @@ -82,13 +83,6 @@ namespace CSMWorld return QString::fromUtf8(Land::createUniqueRecordId(land.mX, land.mY).c_str()); } - template <> - inline QVariant StringIdColumn::get(const Record& record) const - { - const LandTexture& ltex = record.get(); - return QString::fromUtf8(LandTexture::createUniqueRecordId(ltex.mPluginIndex, ltex.mIndex).c_str()); - } - template struct RecordStateColumn : public Column { @@ -570,19 +564,37 @@ namespace CSMWorld QVariant get(const Record& record) const override { - const ESM::Race::MaleFemaleF& value = mWeight ? record.get().mData.mWeight : record.get().mData.mHeight; - - return mMale ? value.mMale : value.mFemale; + if (mWeight) + { + if (mMale) + return record.get().mData.mMaleWeight; + return record.get().mData.mFemaleWeight; + } + if (mMale) + return record.get().mData.mMaleHeight; + return record.get().mData.mFemaleHeight; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - ESM::Race::MaleFemaleF& value = mWeight ? record2.mData.mWeight : record2.mData.mHeight; - - (mMale ? value.mMale : value.mFemale) = data.toFloat(); + float bodyAttr = std::clamp(data.toFloat(), 0.5f, 2.0f); + if (mWeight) + { + if (mMale) + record2.mData.mMaleWeight = bodyAttr; + else + record2.mData.mFemaleWeight = bodyAttr; + } + else + { + if (mMale) + record2.mData.mMaleHeight = bodyAttr; + else + record2.mData.mFemaleHeight = bodyAttr; + } record.setModified(record2); } @@ -952,7 +964,7 @@ namespace CSMWorld void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mScale = data.toFloat(); + record2.mScale = std::clamp(data.toFloat(), 0.5f, 2.0f); record.setModified(record2); } @@ -1095,19 +1107,19 @@ namespace CSMWorld }; template - struct GoldValueColumn : public Column + struct StackSizeColumn : public Column { - GoldValueColumn() - : Column(Columns::ColumnId_CoinValue, ColumnBase::Display_Integer) + StackSizeColumn() + : Column(Columns::ColumnId_StackCount, ColumnBase::Display_Integer) { } - QVariant get(const Record& record) const override { return record.get().mGoldValue; } + QVariant get(const Record& record) const override { return record.get().mCount; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mGoldValue = data.toInt(); + record2.mCount = data.toInt(); record.setModified(record2); } @@ -1117,8 +1129,8 @@ namespace CSMWorld template struct TeleportColumn : public Column { - TeleportColumn() - : Column(Columns::ColumnId_Teleport, ColumnBase::Display_Boolean) + TeleportColumn(int flags) + : Column(Columns::ColumnId_Teleport, ColumnBase::Display_Boolean, flags) { } @@ -1146,6 +1158,8 @@ namespace CSMWorld QVariant get(const Record& record) const override { + if (!record.get().mTeleport) + return QVariant(); return QString::fromUtf8(record.get().mDestCell.c_str()); } @@ -1163,6 +1177,26 @@ namespace CSMWorld bool isUserEditable() const override { return true; } }; + template + struct IsLockedColumn : public Column + { + IsLockedColumn(int flags) + : Column(Columns::ColumnId_IsLocked, ColumnBase::Display_Boolean, flags) + { + } + + QVariant get(const Record& record) const override { return record.get().mIsLocked; } + + void set(Record& record, const QVariant& data) override + { + ESXRecordT record2 = record.get(); + record2.mIsLocked = data.toBool(); + record.setModified(record2); + } + + bool isEditable() const override { return true; } + }; + template struct LockLevelColumn : public Column { @@ -1171,7 +1205,12 @@ namespace CSMWorld { } - QVariant get(const Record& record) const override { return record.get().mLockLevel; } + QVariant get(const Record& record) const override + { + if (record.get().mIsLocked) + return record.get().mLockLevel; + return QVariant(); + } void set(Record& record, const QVariant& data) override { @@ -1193,7 +1232,9 @@ namespace CSMWorld QVariant get(const Record& record) const override { - return QString::fromUtf8(record.get().mKey.getRefIdString().c_str()); + if (record.get().mIsLocked) + return QString::fromUtf8(record.get().mKey.getRefIdString().c_str()); + return QVariant(); } void set(Record& record, const QVariant& data) override @@ -1263,17 +1304,21 @@ namespace CSMWorld { ESM::Position ESXRecordT::*mPosition; int mIndex; + bool mIsDoor; PosColumn(ESM::Position ESXRecordT::*position, int index, bool door) : Column((door ? Columns::ColumnId_DoorPositionXPos : Columns::ColumnId_PositionXPos) + index, ColumnBase::Display_Float) , mPosition(position) , mIndex(index) + , mIsDoor(door) { } QVariant get(const Record& record) const override { + if (!record.get().mTeleport && mIsDoor) + return QVariant(); const ESM::Position& position = record.get().*mPosition; return position.pos[mIndex]; } @@ -1297,17 +1342,21 @@ namespace CSMWorld { ESM::Position ESXRecordT::*mPosition; int mIndex; + bool mIsDoor; RotColumn(ESM::Position ESXRecordT::*position, int index, bool door) : Column((door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot) + index, ColumnBase::Display_Double) , mPosition(position) , mIndex(index) + , mIsDoor(door) { } QVariant get(const Record& record) const override { + if (!record.get().mTeleport && mIsDoor) + return QVariant(); const ESM::Position& position = record.get().*mPosition; return osg::RadiansToDegrees(position.rot[mIndex]); } @@ -2033,6 +2082,26 @@ namespace CSMWorld bool isEditable() const override { return true; } }; + template + struct ProjectileSpeedColumn : public Column + { + ProjectileSpeedColumn() + : Column(Columns::ColumnId_ProjectileSpeed, ColumnBase::Display_Float) + { + } + + QVariant get(const Record& record) const override { return record.get().mData.mSpeed; } + + void set(Record& record, const QVariant& data) override + { + ESXRecordT record2 = record.get(); + record2.mData.mSpeed = data.toFloat(); + record.setModified(record2); + } + + bool isEditable() const override { return true; } + }; + template struct SchoolColumn : public Column { @@ -2289,20 +2358,11 @@ namespace CSMWorld bool isEditable() const override { return true; } }; - struct LandTextureNicknameColumn : public Column - { - LandTextureNicknameColumn(); - - QVariant get(const Record& record) const override; - void set(Record& record, const QVariant& data) override; - bool isEditable() const override; - }; - - struct LandTextureIndexColumn : public Column + struct LandTextureIndexColumn : public Column { LandTextureIndexColumn(); - QVariant get(const Record& record) const override; + QVariant get(const Record& record) const override; bool isEditable() const override; }; @@ -2314,14 +2374,6 @@ namespace CSMWorld bool isEditable() const override; }; - struct LandTexturePluginIndexColumn : public Column - { - LandTexturePluginIndexColumn(); - - QVariant get(const Record& record) const override; - bool isEditable() const override; - }; - struct LandNormalsColumn : public Column { using DataType = QVector; @@ -2376,6 +2428,17 @@ namespace CSMWorld void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; + + struct SelectionGroupColumn : public Column + { + SelectionGroupColumn(); + + QVariant get(const Record& record) const override; + + void set(Record& record, const QVariant& data) override; + + bool isEditable() const override; + }; } // This is required to access the type as a QVariant. diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 4a476b52f3..d4c35c5cec 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -56,9 +56,11 @@ namespace CSMWorld { ColumnId_FactionIndex, "Faction Index" }, { ColumnId_Charges, "Charges" }, { ColumnId_Enchantment, "Enchantment" }, - { ColumnId_CoinValue, "Coin Value" }, + { ColumnId_StackCount, "Count" }, + { ColumnId_GoldValue, "Value" }, { ColumnId_Teleport, "Teleport" }, { ColumnId_TeleportCell, "Teleport Cell" }, + { ColumnId_IsLocked, "Locked" }, { ColumnId_LockLevel, "Lock Level" }, { ColumnId_Key, "Key" }, { ColumnId_Trap, "Trap" }, @@ -235,6 +237,7 @@ namespace CSMWorld { ColumnId_RegionSounds, "Sounds" }, { ColumnId_SoundName, "Sound Name" }, { ColumnId_SoundChance, "Chance" }, + { ColumnId_SoundProbability, "Probability" }, { ColumnId_FactionReactions, "Reactions" }, { ColumnId_FactionRanks, "Ranks" }, @@ -321,7 +324,6 @@ namespace CSMWorld { ColumnId_MaxAttack, "Max Attack" }, { ColumnId_CreatureMisc, "Creature Misc" }, - { ColumnId_Idle1, "Idle 1" }, { ColumnId_Idle2, "Idle 2" }, { ColumnId_Idle3, "Idle 3" }, { ColumnId_Idle4, "Idle 4" }, @@ -329,6 +331,7 @@ namespace CSMWorld { ColumnId_Idle6, "Idle 6" }, { ColumnId_Idle7, "Idle 7" }, { ColumnId_Idle8, "Idle 8" }, + { ColumnId_Idle9, "Idle 9" }, { ColumnId_RegionWeather, "Weather" }, { ColumnId_WeatherName, "Type" }, @@ -376,6 +379,7 @@ namespace CSMWorld { ColumnId_Blocked, "Blocked" }, { ColumnId_LevelledCreatureId, "Levelled Creature" }, + { ColumnId_ProjectileSpeed, "Projectile Speed" }, // end marker { -1, 0 }, diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 92f41a2f20..469c1eee33 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -45,7 +45,7 @@ namespace CSMWorld ColumnId_FactionIndex = 31, ColumnId_Charges = 32, ColumnId_Enchantment = 33, - ColumnId_CoinValue = 34, + ColumnId_StackCount = 34, ColumnId_Teleport = 35, ColumnId_TeleportCell = 36, ColumnId_LockLevel = 37, @@ -310,14 +310,14 @@ namespace CSMWorld ColumnId_MaxAttack = 284, ColumnId_CreatureMisc = 285, - ColumnId_Idle1 = 286, - ColumnId_Idle2 = 287, - ColumnId_Idle3 = 288, - ColumnId_Idle4 = 289, - ColumnId_Idle5 = 290, - ColumnId_Idle6 = 291, - ColumnId_Idle7 = 292, - ColumnId_Idle8 = 293, + ColumnId_Idle2 = 286, + ColumnId_Idle3 = 287, + ColumnId_Idle4 = 288, + ColumnId_Idle5 = 289, + ColumnId_Idle6 = 290, + ColumnId_Idle7 = 291, + ColumnId_Idle8 = 292, + ColumnId_Idle9 = 293, ColumnId_RegionWeather = 294, ColumnId_WeatherName = 295, @@ -347,6 +347,16 @@ namespace CSMWorld ColumnId_LevelledCreatureId = 315, + ColumnId_SelectionGroupObjects = 316, + + ColumnId_SoundProbability = 317, + + ColumnId_IsLocked = 318, + + ColumnId_ProjectileSpeed = 319, + + ColumnId_GoldValue = 320, + // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index da49caef10..168049641a 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -36,7 +35,7 @@ CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUnd void CSMWorld::TouchCommand::redo() { - mOld.reset(mTable.getRecord(mId).clone().get()); + mOld = mTable.getRecord(mId).clone(); mChanged = mTable.touchRecord(mId); } @@ -61,11 +60,11 @@ CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand( void CSMWorld::ImportLandTexturesCommand::redo() { - int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); - int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); + const int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); + const int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); // Original data - int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); + const int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); mOld = mLands.data(mLands.getModelIndex(getOriginId(), textureColumn)).value(); // Need to make a copy so the old values can be looked up @@ -74,44 +73,37 @@ void CSMWorld::ImportLandTexturesCommand::redo() // Perform touch/copy/etc... onRedo(); - // Find all indices used - std::unordered_set texIndices; - for (int i = 0; i < mOld.size(); ++i) + std::unordered_map indexMapping; + for (uint16_t index : mOld) { // All indices are offset by 1 for a default texture - if (mOld[i] > 0) - texIndices.insert(mOld[i] - 1); - } - - std::vector oldTextures; - oldTextures.reserve(texIndices.size()); - for (int index : texIndices) - { - oldTextures.push_back(LandTexture::createUniqueRecordId(oldPlugin, index)); - } - - // Import the textures, replace old values - LandTextureIdTable::ImportResults results = dynamic_cast(mLtexs).importTextures(oldTextures); - mCreatedTextures = std::move(results.createdRecords); - for (const auto& it : results.recordMapping) - { - int plugin = 0, newIndex = 0, oldIndex = 0; - LandTexture::parseUniqueRecordId(it.first, plugin, oldIndex); - LandTexture::parseUniqueRecordId(it.second, plugin, newIndex); - - if (newIndex != oldIndex) + if (index == 0) + continue; + if (indexMapping.contains(index)) + continue; + const CSMWorld::Record* record + = static_cast(mLtexs).searchRecord(index - 1, oldPlugin); + if (!record || record->isDeleted()) { - for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i) - { - // All indices are offset by 1 for a default texture - if (mOld[i] == oldIndex + 1) - copy[i] = newIndex + 1; - } + indexMapping.emplace(index, 0); + continue; } + if (!record->isModified()) + { + mTouchedTextures.emplace_back(record->clone()); + mLtexs.touchRecord(record->get().mId.getRefIdString()); + } + indexMapping.emplace(index, record->get().mIndex + 1); + } + for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i) + { + uint16_t oldIndex = mOld[i]; + uint16_t newIndex = indexMapping[oldIndex]; + copy[i] = newIndex; } // Apply modification - int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); + const int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); mOldState = mLands.data(mLands.getModelIndex(getDestinationId(), stateColumn)).toInt(); QVariant variant; @@ -133,12 +125,12 @@ void CSMWorld::ImportLandTexturesCommand::undo() // Undo copy/touch/etc... onUndo(); - for (const std::string& id : mCreatedTextures) + for (auto& ltex : mTouchedTextures) { - int row = mLtexs.getModelIndex(id, 0).row(); - mLtexs.removeRows(row, 1); + ESM::RefId id = static_cast*>(ltex.get())->get().mId; + mLtexs.setRecord(id.getRefIdString(), std::move(ltex)); } - mCreatedTextures.clear(); + mTouchedTextures.clear(); } CSMWorld::CopyLandTexturesCommand::CopyLandTexturesCommand( @@ -181,9 +173,8 @@ const std::string& CSMWorld::TouchLandCommand::getDestinationId() const void CSMWorld::TouchLandCommand::onRedo() { + mOld = mLands.getRecord(mId).clone(); mChanged = mLands.touchRecord(mId); - if (mChanged) - mOld.reset(mLands.getRecord(mId).clone().get()); } void CSMWorld::TouchLandCommand::onUndo() diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index a243950003..f6b0caeb1a 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -69,7 +69,7 @@ namespace CSMWorld IdTable& mLtexs; DataType mOld; int mOldState; - std::vector mCreatedTextures; + std::vector> mTouchedTextures; }; /// \brief This command is used to fix LandTexture records and texture diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 07e9879795..00e5fec7b0 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -36,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -137,17 +137,19 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data : mEncoder(encoding) , mPathgrids(mCells) , mRefs(mCells) - , mReader(nullptr) , mDialogue(nullptr) - , mReaderIndex(1) + , mReaderIndex(0) , mDataPaths(dataPaths) , mArchives(archives) + , mVFS(std::make_unique()) { - mVFS = std::make_unique(); VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true); mResourcesManager.setVFS(mVFS.get()); - mResourceSystem = std::make_unique(mVFS.get()); + + constexpr double expiryDelay = 0; + mResourceSystem + = std::make_unique(mVFS.get(), expiryDelay, &mEncoder.getStatelessEncoder()); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); @@ -158,7 +160,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data defines["radialFog"] = "0"; defines["lightingModel"] = "0"; defines["reverseZ"] = "0"; - defines["refraction_enabled"] = "0"; + defines["waterRefraction"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); @@ -298,8 +300,8 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mRegions.addColumn(new NestedParentColumn(Columns::ColumnId_RegionWeather)); index = mRegions.getColumns() - 1; mRegions.addAdapter(std::make_pair(&mRegions.getColumn(index), new RegionWeatherAdapter())); - mRegions.getNestableColumn(index)->addColumn( - new NestedChildColumn(Columns::ColumnId_WeatherName, ColumnBase::Display_String, false)); + mRegions.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_WeatherName, ColumnBase::Display_String, ColumnBase::Flag_Dialogue, false)); mRegions.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_WeatherChance, ColumnBase::Display_UnsignedInteger8)); // Region Sounds @@ -310,6 +312,8 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data new NestedChildColumn(Columns::ColumnId_SoundName, ColumnBase::Display_Sound)); mRegions.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_SoundChance, ColumnBase::Display_UnsignedInteger8)); + mRegions.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_SoundProbability, ColumnBase::Display_String, ColumnBase::Flag_Dialogue, false)); mBirthsigns.addColumn(new StringIdColumn); mBirthsigns.addColumn(new RecordStateColumn); @@ -497,6 +501,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mMagicEffects.addColumn(new FixedRecordTypeColumn(UniversalId::Type_MagicEffect)); mMagicEffects.addColumn(new SchoolColumn); mMagicEffects.addColumn(new BaseCostColumn); + mMagicEffects.addColumn(new ProjectileSpeedColumn); mMagicEffects.addColumn(new EffectTextureColumn(Columns::ColumnId_Icon)); mMagicEffects.addColumn(new EffectTextureColumn(Columns::ColumnId_Particle)); mMagicEffects.addColumn(new EffectObjectColumn(Columns::ColumnId_CastingObject)); @@ -507,6 +512,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_HitSound)); mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_AreaSound)); mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_BoltSound)); + mMagicEffects.addColumn( new FlagColumn(Columns::ColumnId_AllowSpellmaking, ESM::MagicEffect::AllowSpellmaking)); mMagicEffects.addColumn( @@ -524,13 +530,11 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mLand.addColumn(new LandColoursColumn); mLand.addColumn(new LandTexturesColumn); - mLandTextures.addColumn(new StringIdColumn(true)); - mLandTextures.addColumn(new RecordStateColumn); - mLandTextures.addColumn(new FixedRecordTypeColumn(UniversalId::Type_LandTexture)); - mLandTextures.addColumn(new LandTextureNicknameColumn); - mLandTextures.addColumn(new LandTexturePluginIndexColumn); + mLandTextures.addColumn(new StringIdColumn); + mLandTextures.addColumn(new RecordStateColumn); + mLandTextures.addColumn(new FixedRecordTypeColumn(UniversalId::Type_LandTexture)); mLandTextures.addColumn(new LandTextureIndexColumn); - mLandTextures.addColumn(new TextureColumn); + mLandTextures.addColumn(new TextureColumn); mPathgrids.addColumn(new StringIdColumn); mPathgrids.addColumn(new RecordStateColumn); @@ -585,8 +589,9 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mRefs.addColumn(new FactionIndexColumn); mRefs.addColumn(new ChargesColumn); mRefs.addColumn(new EnchantmentChargesColumn); - mRefs.addColumn(new GoldValueColumn); - mRefs.addColumn(new TeleportColumn); + mRefs.addColumn(new StackSizeColumn); + mRefs.addColumn(new TeleportColumn( + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mRefs.addColumn(new TeleportCellColumn); mRefs.addColumn(new PosColumn(&CellRef::mDoorDest, 0, true)); mRefs.addColumn(new PosColumn(&CellRef::mDoorDest, 1, true)); @@ -594,6 +599,8 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mRefs.addColumn(new RotColumn(&CellRef::mDoorDest, 0, true)); mRefs.addColumn(new RotColumn(&CellRef::mDoorDest, 1, true)); mRefs.addColumn(new RotColumn(&CellRef::mDoorDest, 2, true)); + mRefs.addColumn(new IsLockedColumn( + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mRefs.addColumn(new LockLevelColumn); mRefs.addColumn(new KeyColumn); mRefs.addColumn(new TrapColumn); @@ -618,6 +625,11 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data mDebugProfiles.addColumn(new DescriptionColumn); mDebugProfiles.addColumn(new ScriptColumn(ScriptColumn::Type_Lines)); + mSelectionGroups.addColumn(new StringIdColumn); + mSelectionGroups.addColumn(new RecordStateColumn); + mSelectionGroups.addColumn(new FixedRecordTypeColumn(UniversalId::Type_SelectionGroup)); + mSelectionGroups.addColumn(new SelectionGroupColumn); + mMetaData.appendBlankRecord(ESM::RefId::stringRefId("sys::meta")); mMetaData.addColumn(new StringIdColumn(true)); @@ -662,6 +674,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Textures)), UniversalId::Type_Texture); addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Videos)), UniversalId::Type_Video); addModel(new IdTable(&mMetaData), UniversalId::Type_MetaData); + addModel(new IdTable(&mSelectionGroups), UniversalId::Type_SelectionGroup); mActorAdapter = std::make_unique(*this); @@ -672,8 +685,6 @@ CSMWorld::Data::~Data() { for (std::vector::iterator iter(mModels.begin()); iter != mModels.end(); ++iter) delete *iter; - - delete mReader; } std::shared_ptr CSMWorld::Data::getResourceSystem() @@ -906,6 +917,16 @@ CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() return mDebugProfiles; } +CSMWorld::IdCollection& CSMWorld::Data::getSelectionGroups() +{ + return mSelectionGroups; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getSelectionGroups() const +{ + return mSelectionGroups; +} + const CSMWorld::IdCollection& CSMWorld::Data::getLand() const { return mLand; @@ -916,12 +937,12 @@ CSMWorld::IdCollection& CSMWorld::Data::getLand() return mLand; } -const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const +const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const { return mLandTextures; } -CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() +CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() { return mLandTextures; } @@ -1042,17 +1063,11 @@ int CSMWorld::Data::startLoading(const std::filesystem::path& path, bool base, b { Log(Debug::Info) << "Loading content file " << path; - // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading - std::shared_ptr ptr(mReader); - mReaders.push_back(ptr); - mReader = nullptr; - mDialogue = nullptr; - mReader = new ESM::ESMReader; - mReader->setEncoder(&mEncoder); - mReader->setIndex((project || !base) ? 0 : mReaderIndex++); - mReader->open(path); + ESM::ReadersCache::BusyItem reader = mReaders.get(mReaderIndex++); + reader->setEncoder(&mEncoder); + reader->open(path); mBase = base; mProject = project; @@ -1061,13 +1076,13 @@ int CSMWorld::Data::startLoading(const std::filesystem::path& path, bool base, b { MetaData metaData; metaData.mId = ESM::RefId::stringRefId("sys::meta"); - metaData.load(*mReader); + metaData.load(*reader); mMetaData.setRecord(0, std::make_unique>(Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } - return mReader->getRecordCount(); + return reader->getRecordCount(); } void CSMWorld::Data::loadFallbackEntries() @@ -1089,7 +1104,7 @@ void CSMWorld::Data::loadFallbackEntries() newMarker.mModel = model; newMarker.mRecordFlags = 0; auto record = std::make_unique>(); - record->mBase = newMarker; + record->mBase = std::move(newMarker); record->mState = CSMWorld::RecordBase::State_BaseOnly; mReferenceables.appendRecord(std::move(record), CSMWorld::UniversalId::Type_Static); } @@ -1105,7 +1120,7 @@ void CSMWorld::Data::loadFallbackEntries() newMarker.mModel = model; newMarker.mRecordFlags = 0; auto record = std::make_unique>(); - record->mBase = newMarker; + record->mBase = std::move(newMarker); record->mState = CSMWorld::RecordBase::State_BaseOnly; mReferenceables.appendRecord(std::move(record), CSMWorld::UniversalId::Type_Door); } @@ -1114,24 +1129,17 @@ void CSMWorld::Data::loadFallbackEntries() bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) { - if (!mReader) + if (mReaderIndex == 0) throw std::logic_error("can't continue loading, because no load has been started"); + ESM::ReadersCache::BusyItem reader = mReaders.get(mReaderIndex - 1); + if (!reader->isOpen()) + throw std::logic_error("can't continue loading, because no load has been started"); + reader->setEncoder(&mEncoder); + reader->setIndex(static_cast(mReaderIndex - 1)); + reader->resolveParentFileIndices(mReaders); - if (!mReader->hasMoreRecs()) + if (!reader->hasMoreRecs()) { - if (mBase) - { - // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand - // loading. We don't store non-base reader, because everything going into modified will be fully loaded - // during the initial loading process. - std::shared_ptr ptr(mReader); - mReaders.push_back(ptr); - } - else - delete mReader; - - mReader = nullptr; - mDialogue = nullptr; loadFallbackEntries(); @@ -1139,76 +1147,76 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) return true; } - ESM::NAME n = mReader->getRecName(); - mReader->getRecHeader(); + ESM::NAME n = reader->getRecName(); + reader->getRecHeader(); bool unhandledRecord = false; switch (n.toInt()) { case ESM::REC_GLOB: - mGlobals.load(*mReader, mBase); + mGlobals.load(*reader, mBase); break; case ESM::REC_GMST: - mGmsts.load(*mReader, mBase); + mGmsts.load(*reader, mBase); break; case ESM::REC_SKIL: - mSkills.load(*mReader, mBase); + mSkills.load(*reader, mBase); break; case ESM::REC_CLAS: - mClasses.load(*mReader, mBase); + mClasses.load(*reader, mBase); break; case ESM::REC_FACT: - mFactions.load(*mReader, mBase); + mFactions.load(*reader, mBase); break; case ESM::REC_RACE: - mRaces.load(*mReader, mBase); + mRaces.load(*reader, mBase); break; case ESM::REC_SOUN: - mSounds.load(*mReader, mBase); + mSounds.load(*reader, mBase); break; case ESM::REC_SCPT: - mScripts.load(*mReader, mBase); + mScripts.load(*reader, mBase); break; case ESM::REC_REGN: - mRegions.load(*mReader, mBase); + mRegions.load(*reader, mBase); break; case ESM::REC_BSGN: - mBirthsigns.load(*mReader, mBase); + mBirthsigns.load(*reader, mBase); break; case ESM::REC_SPEL: - mSpells.load(*mReader, mBase); + mSpells.load(*reader, mBase); break; case ESM::REC_ENCH: - mEnchantments.load(*mReader, mBase); + mEnchantments.load(*reader, mBase); break; case ESM::REC_BODY: - mBodyParts.load(*mReader, mBase); + mBodyParts.load(*reader, mBase); break; case ESM::REC_SNDG: - mSoundGens.load(*mReader, mBase); + mSoundGens.load(*reader, mBase); break; case ESM::REC_MGEF: - mMagicEffects.load(*mReader, mBase); + mMagicEffects.load(*reader, mBase); break; case ESM::REC_PGRD: - mPathgrids.load(*mReader, mBase); + mPathgrids.load(*reader, mBase); break; case ESM::REC_SSCR: - mStartScripts.load(*mReader, mBase); + mStartScripts.load(*reader, mBase); break; case ESM::REC_LTEX: - mLandTextures.load(*mReader, mBase); + mLandTextures.load(*reader, mBase); break; case ESM::REC_LAND: - mLand.load(*mReader, mBase); + mLand.load(*reader, mBase); break; case ESM::REC_CELL: { - int index = mCells.load(*mReader, mBase); + int index = mCells.load(*reader, mBase); if (index < 0 || index >= mCells.getSize()) { // log an error and continue loading the refs to the last loaded cell @@ -1217,69 +1225,69 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) index = mCells.getSize() - 1; } - mRefs.load(*mReader, index, mBase, mRefLoadCache[mCells.getId(index)], messages); + mRefs.load(*reader, index, mBase, mRefLoadCache[mCells.getId(index)], messages); break; } case ESM::REC_ACTI: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Activator); + mReferenceables.load(*reader, mBase, UniversalId::Type_Activator); break; case ESM::REC_ALCH: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Potion); + mReferenceables.load(*reader, mBase, UniversalId::Type_Potion); break; case ESM::REC_APPA: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Apparatus); + mReferenceables.load(*reader, mBase, UniversalId::Type_Apparatus); break; case ESM::REC_ARMO: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Armor); + mReferenceables.load(*reader, mBase, UniversalId::Type_Armor); break; case ESM::REC_BOOK: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Book); + mReferenceables.load(*reader, mBase, UniversalId::Type_Book); break; case ESM::REC_CLOT: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Clothing); + mReferenceables.load(*reader, mBase, UniversalId::Type_Clothing); break; case ESM::REC_CONT: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Container); + mReferenceables.load(*reader, mBase, UniversalId::Type_Container); break; case ESM::REC_CREA: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Creature); + mReferenceables.load(*reader, mBase, UniversalId::Type_Creature); break; case ESM::REC_DOOR: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Door); + mReferenceables.load(*reader, mBase, UniversalId::Type_Door); break; case ESM::REC_INGR: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Ingredient); + mReferenceables.load(*reader, mBase, UniversalId::Type_Ingredient); break; case ESM::REC_LEVC: - mReferenceables.load(*mReader, mBase, UniversalId::Type_CreatureLevelledList); + mReferenceables.load(*reader, mBase, UniversalId::Type_CreatureLevelledList); break; case ESM::REC_LEVI: - mReferenceables.load(*mReader, mBase, UniversalId::Type_ItemLevelledList); + mReferenceables.load(*reader, mBase, UniversalId::Type_ItemLevelledList); break; case ESM::REC_LIGH: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Light); + mReferenceables.load(*reader, mBase, UniversalId::Type_Light); break; case ESM::REC_LOCK: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Lockpick); + mReferenceables.load(*reader, mBase, UniversalId::Type_Lockpick); break; case ESM::REC_MISC: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Miscellaneous); + mReferenceables.load(*reader, mBase, UniversalId::Type_Miscellaneous); break; case ESM::REC_NPC_: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Npc); + mReferenceables.load(*reader, mBase, UniversalId::Type_Npc); break; case ESM::REC_PROB: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Probe); + mReferenceables.load(*reader, mBase, UniversalId::Type_Probe); break; case ESM::REC_REPA: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Repair); + mReferenceables.load(*reader, mBase, UniversalId::Type_Repair); break; case ESM::REC_STAT: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Static); + mReferenceables.load(*reader, mBase, UniversalId::Type_Static); break; case ESM::REC_WEAP: - mReferenceables.load(*mReader, mBase, UniversalId::Type_Weapon); + mReferenceables.load(*reader, mBase, UniversalId::Type_Weapon); break; case ESM::REC_DIAL: @@ -1287,7 +1295,7 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) ESM::Dialogue record; bool isDeleted = false; - record.load(*mReader, isDeleted); + record.load(*reader, isDeleted); if (isDeleted) { @@ -1333,14 +1341,14 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) messages.add(UniversalId::Type_None, "Found info record not following a dialogue record", "", CSMDoc::Message::Severity_Error); - mReader->skipRecord(); + reader->skipRecord(); break; } if (mDialogue->mType == ESM::Dialogue::Journal) - mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfoOrder); + mJournalInfos.load(*reader, mBase, *mDialogue, mJournalInfoOrder); else - mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfoOrder); + mTopicInfos.load(*reader, mBase, *mDialogue, mTopicInfoOrder); break; } @@ -1353,7 +1361,7 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) break; } - mFilters.load(*mReader, mBase); + mFilters.load(*reader, mBase); break; case ESM::REC_DBGP: @@ -1364,7 +1372,18 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) break; } - mDebugProfiles.load(*mReader, mBase); + mDebugProfiles.load(*reader, mBase); + break; + + case ESM::REC_SELG: + + if (!mProject) + { + unhandledRecord = true; + break; + } + + mSelectionGroups.load(*reader, mBase); break; default: @@ -1377,7 +1396,7 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) messages.add( UniversalId::Type_None, "Unsupported record type: " + n.toString(), "", CSMDoc::Message::Severity_Error); - mReader->skipRecord(); + reader->skipRecord(); } return false; @@ -1387,6 +1406,8 @@ void CSMWorld::Data::finishLoading() { mTopicInfos.sort(mTopicInfoOrder); mJournalInfos.sort(mJournalInfoOrder); + // Release file locks so we can actually write to the file we're editing + mReaders.clear(); } bool CSMWorld::Data::hasId(const std::string& id) const @@ -1409,7 +1430,8 @@ int CSMWorld::Data::count(RecordBase::State state) const + count(state, mRegions) + count(state, mBirthsigns) + count(state, mSpells) + count(state, mCells) + count(state, mEnchantments) + count(state, mBodyParts) + count(state, mLand) + count(state, mLandTextures) + count(state, mSoundGens) + count(state, mMagicEffects) + count(state, mReferenceables) - + count(state, mPathgrids); + + count(state, mPathgrids) + count(state, mTopics) + count(state, mTopicInfos) + count(state, mJournals) + + count(state, mJournalInfos); } std::vector CSMWorld::Data::getIds(bool listDeleted) const diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 1b63986eac..237b746746 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,7 +44,6 @@ #include "idcollection.hpp" #include "infocollection.hpp" #include "land.hpp" -#include "landtexture.hpp" #include "metadata.hpp" #include "nestedidcollection.hpp" #include "nestedinfocollection.hpp" @@ -105,13 +107,14 @@ namespace CSMWorld IdCollection mBodyParts; IdCollection mMagicEffects; IdCollection mDebugProfiles; + IdCollection mSelectionGroups; IdCollection mSoundGens; IdCollection mStartScripts; NestedInfoCollection mTopicInfos; InfoCollection mJournalInfos; NestedIdCollection mCells; SubCellCollection mPathgrids; - IdCollection mLandTextures; + IdCollection mLandTextures; IdCollection mLand; RefIdCollection mReferenceables; RefCollection mRefs; @@ -120,12 +123,12 @@ namespace CSMWorld std::unique_ptr mActorAdapter; std::vector mModels; std::map mModelIndex; - ESM::ESMReader* mReader; + ESM::ReadersCache mReaders; const ESM::Dialogue* mDialogue; // last loaded dialogue bool mBase; bool mProject; - std::map> mRefLoadCache; - int mReaderIndex; + std::map> mRefLoadCache; + std::size_t mReaderIndex; Files::PathContainer mDataPaths; std::vector mArchives; @@ -133,14 +136,11 @@ namespace CSMWorld ResourcesManager mResourcesManager; std::shared_ptr mResourceSystem; - std::vector> mReaders; - InfoOrderByTopic mJournalInfoOrder; InfoOrderByTopic mTopicInfoOrder; - // not implemented - Data(const Data&); - Data& operator=(const Data&); + Data(const Data&) = delete; + Data& operator=(const Data&) = delete; void addModel(QAbstractItemModel* model, UniversalId::Type type, bool update = true); @@ -251,13 +251,17 @@ namespace CSMWorld IdCollection& getDebugProfiles(); + const IdCollection& getSelectionGroups() const; + + IdCollection& getSelectionGroups(); + const IdCollection& getLand() const; IdCollection& getLand(); - const IdCollection& getLandTextures() const; + const IdCollection& getLandTextures() const; - IdCollection& getLandTextures(); + IdCollection& getLandTextures(); const IdCollection& getSoundGens() const; diff --git a/apps/opencs/model/world/disabletag.hpp b/apps/opencs/model/world/disabletag.hpp new file mode 100644 index 0000000000..1aee1ce6fc --- /dev/null +++ b/apps/opencs/model/world/disabletag.hpp @@ -0,0 +1,22 @@ +#ifndef CSM_WOLRD_DISABLETAG_H +#define CSM_WOLRD_DISABLETAG_H + +#include + +namespace CSMWorld +{ + class DisableTag + { + public: + static QVariant getVariant() { return QVariant::fromValue(CSMWorld::DisableTag()); } + + static bool isDisableTag(const QVariant& variant) + { + return strcmp(variant.typeName(), "CSMWorld::DisableTag") == 0; + } + }; +} + +Q_DECLARE_METATYPE(CSMWorld::DisableTag) + +#endif diff --git a/apps/opencs/model/world/idcollection.cpp b/apps/opencs/model/world/idcollection.cpp index faab5a20cb..ca56eceb69 100644 --- a/apps/opencs/model/world/idcollection.cpp +++ b/apps/opencs/model/world/idcollection.cpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace ESM @@ -18,12 +19,12 @@ namespace ESM namespace CSMWorld { template <> - int IdCollection::load(ESM::ESMReader& reader, bool base) + int BaseIdCollection::load(ESM::ESMReader& reader, bool base) { Pathgrid record; bool isDeleted = false; - loadRecord(record, reader, isDeleted); + loadRecord(record, reader, isDeleted, base); const ESM::RefId id = getRecordId(record); int index = this->searchId(id); @@ -55,4 +56,115 @@ namespace CSMWorld return load(record, base, index); } + + const Record* IdCollection::searchRecord(std::uint16_t index, int plugin) const + { + auto found = mIndices.find({ plugin, index }); + if (found != mIndices.end()) + { + int index = searchId(found->second); + if (index != -1) + return &getRecord(index); + } + return nullptr; + } + + const std::string* IdCollection::getLandTexture(std::uint16_t index, int plugin) const + { + const Record* record = searchRecord(index, plugin); + if (record && !record->isDeleted()) + return &record->get().mTexture; + return nullptr; + } + + void IdCollection::loadRecord( + ESM::LandTexture& record, ESM::ESMReader& reader, bool& isDeleted, bool base) + { + record.load(reader, isDeleted); + int plugin = base ? reader.getIndex() : -1; + mIndices.emplace(std::make_pair(plugin, record.mIndex), record.mId); + } + + std::uint16_t IdCollection::assignNewIndex(ESM::RefId id) + { + std::uint16_t index = 0; + if (!mIndices.empty()) + { + auto end = mIndices.lower_bound({ -1, std::numeric_limits::max() }); + if (end != mIndices.begin()) + end = std::prev(end); + if (end->first.first == -1) + { + constexpr std::uint16_t maxIndex = std::numeric_limits::max() - 1; + if (end->first.second < maxIndex) + index = end->first.second + 1; + else + { + std::uint16_t prevIndex = 0; + for (auto it = mIndices.lower_bound({ -1, 0 }); it != end; ++it) + { + if (prevIndex != it->first.second) + { + index = prevIndex; + break; + } + ++prevIndex; + } + } + } + } + mIndices.emplace(std::make_pair(-1, index), id); + return index; + } + + bool IdCollection::touchRecord(const ESM::RefId& id) + { + int row = BaseIdCollection::touchRecordImp(id); + if (row != -1) + { + const_cast(getRecord(row).get()).mIndex = assignNewIndex(id); + return true; + } + return false; + } + + void IdCollection::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) + { + int row = cloneRecordImp(origin, destination, type); + const_cast(getRecord(row).get()).mIndex = assignNewIndex(destination); + } + + void IdCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) + { + ESM::LandTexture record; + record.blank(); + record.mId = id; + record.mIndex = assignNewIndex(id); + + auto record2 = std::make_unique>(); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = std::move(record); + + insertRecord(std::move(record2), getAppendIndex(id, type), type); + } + + void IdCollection::removeRows(int index, int count) + { + for (int row = index; row < index + count; ++row) + { + const auto& record = getRecord(row); + if (record.isModified()) + mIndices.erase({ -1, record.get().mIndex }); + } + BaseIdCollection::removeRows(index, count); + } + + void IdCollection::replace(int index, std::unique_ptr record) + { + const auto& current = getRecord(index); + if (current.isModified() && !record->isModified()) + mIndices.erase({ -1, current.get().mIndex }); + BaseIdCollection::replace(index, std::move(record)); + } } diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index 67b886c3f8..d4aa82ffc9 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -17,6 +17,7 @@ namespace ESM { class ESMReader; + struct LandTexture; } namespace CSMWorld @@ -25,9 +26,9 @@ namespace CSMWorld /// \brief Single type collection of top level records template - class IdCollection : public Collection + class BaseIdCollection : public Collection { - virtual void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted); + virtual void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base); public: /// \return Index of loaded record (-1 if no record was loaded) @@ -46,14 +47,46 @@ namespace CSMWorld /// \return Has the ID been deleted? }; + template + class IdCollection : public BaseIdCollection + { + }; + + template <> + class IdCollection : public BaseIdCollection + { + std::map, ESM::RefId> mIndices; + + void loadRecord(ESM::LandTexture& record, ESM::ESMReader& reader, bool& isDeleted, bool base) override; + + std::uint16_t assignNewIndex(ESM::RefId id); + + public: + const Record* searchRecord(std::uint16_t index, int plugin) const; + + const std::string* getLandTexture(std::uint16_t index, int plugin) const; + + bool touchRecord(const ESM::RefId& id) override; + + void cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; + + void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) override; + + void removeRows(int index, int count) override; + + void replace(int index, std::unique_ptr record) override; + }; + template - void IdCollection::loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) + void BaseIdCollection::loadRecord( + ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { record.load(reader, isDeleted); } template <> - inline void IdCollection::loadRecord(Land& record, ESM::ESMReader& reader, bool& isDeleted) + inline void BaseIdCollection::loadRecord(Land& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { record.load(reader, isDeleted); @@ -64,15 +97,17 @@ namespace CSMWorld // Prevent data from being reloaded. record.mContext.filename.clear(); + if (!base) + record.setPlugin(-1); } template - int IdCollection::load(ESM::ESMReader& reader, bool base) + int BaseIdCollection::load(ESM::ESMReader& reader, bool base) { ESXRecordT record; bool isDeleted = false; - loadRecord(record, reader, isDeleted); + loadRecord(record, reader, isDeleted, base); ESM::RefId id = getRecordId(record); int index = this->searchId(id); @@ -103,7 +138,7 @@ namespace CSMWorld } template - int IdCollection::load(const ESXRecordT& record, bool base, int index) + int BaseIdCollection::load(const ESXRecordT& record, bool base, int index) { if (index == -2) // index unknown index = this->searchId(getRecordId(record)); @@ -135,7 +170,7 @@ namespace CSMWorld } template - bool IdCollection::tryDelete(const ESM::RefId& id) + bool BaseIdCollection::tryDelete(const ESM::RefId& id) { int index = this->searchId(id); @@ -162,7 +197,7 @@ namespace CSMWorld } template <> - int IdCollection::load(ESM::ESMReader& reader, bool base); + int BaseIdCollection::load(ESM::ESMReader& reader, bool base); } #endif diff --git a/apps/opencs/model/world/idcompletionmanager.cpp b/apps/opencs/model/world/idcompletionmanager.cpp index 263f462b6e..a4fdb4776d 100644 --- a/apps/opencs/model/world/idcompletionmanager.cpp +++ b/apps/opencs/model/world/idcompletionmanager.cpp @@ -117,7 +117,7 @@ void CSMWorld::IdCompletionManager::generateCompleters(CSMWorld::Data& data) completer->setPopup(popup); // The completer takes ownership of the popup completer->setMaxVisibleItems(10); - mCompleters[current->first] = completer; + mCompleters[current->first] = std::move(completer); } } } diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 69ac8a42b6..5ed0c6f0e7 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include "collectionbase.hpp" #include "columnbase.hpp" -#include "landtexture.hpp" CSMWorld::IdTable::IdTable(CollectionBase* idCollection, unsigned int features) : IdTableBase(features) @@ -361,68 +361,8 @@ CSMWorld::LandTextureIdTable::LandTextureIdTable(CollectionBase* idCollection, u { } -CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::importTextures( - const std::vector& ids) +const CSMWorld::Record* CSMWorld::LandTextureIdTable::searchRecord( + std::uint16_t index, int plugin) const { - ImportResults results; - - // Map existing textures to ids - std::map reverseLookupMap; - for (int i = 0; i < idCollection()->getSize(); ++i) - { - auto& record = static_cast&>(idCollection()->getRecord(i)); - if (record.isModified()) - reverseLookupMap.emplace( - Misc::StringUtils::lowerCase(record.get().mTexture), idCollection()->getId(i).getRefIdString()); - } - - for (const std::string& id : ids) - { - int plugin, index; - LandTexture::parseUniqueRecordId(id, plugin, index); - - const ESM::RefId refId = ESM::RefId::stringRefId(id); - const int oldRow = idCollection()->searchId(refId); - - // If it does not exist or it is in the current plugin, it can be skipped. - if (oldRow < 0 || plugin == 0) - { - results.recordMapping.emplace_back(id, id); - continue; - } - - // Look for a pre-existing record - auto& record = static_cast&>(idCollection()->getRecord(oldRow)); - std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture); - auto searchIt = reverseLookupMap.find(texture); - if (searchIt != reverseLookupMap.end()) - { - results.recordMapping.emplace_back(id, searchIt->second); - continue; - } - - // Iterate until an unused index or found, or the index has completely wrapped around. - int startIndex = index; - do - { - std::string newId = LandTexture::createUniqueRecordId(0, index); - const ESM::RefId newRefId = ESM::RefId::stringRefId(newId); - int newRow = idCollection()->searchId(newRefId); - - if (newRow < 0) - { - // Id not taken, clone it - cloneRecord(refId, newRefId, UniversalId::Type_LandTexture); - results.createdRecords.push_back(newId); - results.recordMapping.emplace_back(id, newId); - reverseLookupMap.emplace(texture, newId); - break; - } - - const size_t MaxIndex = std::numeric_limits::max() - 1; - index = (index + 1) % MaxIndex; - } while (index != startIndex); - } - - return results; + return static_cast*>(idCollection())->searchRecord(index, plugin); } diff --git a/apps/opencs/model/world/idtable.hpp b/apps/opencs/model/world/idtable.hpp index 3ec075ca95..9247c6c1e8 100644 --- a/apps/opencs/model/world/idtable.hpp +++ b/apps/opencs/model/world/idtable.hpp @@ -16,10 +16,17 @@ class QObject; +namespace ESM +{ + struct LandTexture; +} + namespace CSMWorld { class CollectionBase; struct RecordBase; + template + struct Record; class IdTable : public IdTableBase { @@ -103,26 +110,12 @@ namespace CSMWorld virtual CollectionBase* idCollection() const; }; - /// An IdTable customized to handle the more unique needs of LandTextureId's which behave - /// differently from other records. The major difference is that base records cannot be - /// modified. class LandTextureIdTable : public IdTable { public: - struct ImportResults - { - using StringPair = std::pair; - - /// The newly added records - std::vector createdRecords; - /// The 1st string is the original id, the 2nd is the mapped id - std::vector recordMapping; - }; - LandTextureIdTable(CollectionBase* idCollection, unsigned int features = 0); - /// Finds and maps/recreates the specified ids. - ImportResults importTextures(const std::vector& ids); + const CSMWorld::Record* searchRecord(std::uint16_t index, int plugin) const; }; } diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index d4e342f5de..c03b4ea30a 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -63,9 +63,18 @@ bool CSMWorld::IdTableProxyModel::filterAcceptsRow(int sourceRow, const QModelIn CSMWorld::IdTableProxyModel::IdTableProxyModel(QObject* parent) : QSortFilterProxyModel(parent) + , mFilterTimer{ new QTimer(this) } , mSourceModel(nullptr) { setSortCaseSensitivity(Qt::CaseInsensitive); + + mFilterTimer->setSingleShot(true); + int intervalSetting = CSMPrefs::State::get()["ID Tables"]["filter-delay"].toInt(); + mFilterTimer->setInterval(intervalSetting); + + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, + [this](const CSMPrefs::Setting* setting) { this->settingChanged(setting); }); + connect(mFilterTimer.get(), &QTimer::timeout, this, [this]() { this->timerTimeout(); }); } QModelIndex CSMWorld::IdTableProxyModel::getModelIndex(const std::string& id, int column) const @@ -87,10 +96,8 @@ void CSMWorld::IdTableProxyModel::setSourceModel(QAbstractItemModel* model) void CSMWorld::IdTableProxyModel::setFilter(const std::shared_ptr& filter) { - beginResetModel(); - mFilter = filter; - updateColumnMap(); - endResetModel(); + mAwaitingFilter = filter; + mFilterTimer->start(); } bool CSMWorld::IdTableProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const @@ -131,6 +138,26 @@ void CSMWorld::IdTableProxyModel::refreshFilter() } } +void CSMWorld::IdTableProxyModel::timerTimeout() +{ + if (mAwaitingFilter) + { + beginResetModel(); + mFilter = mAwaitingFilter; + updateColumnMap(); + endResetModel(); + mAwaitingFilter.reset(); + } +} + +void CSMWorld::IdTableProxyModel::settingChanged(const CSMPrefs::Setting* setting) +{ + if (*setting == "ID Tables/filter-delay") + { + mFilterTimer->setInterval(setting->toInt()); + } +} + void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex& parent, int /*start*/, int end) { refreshFilter(); diff --git a/apps/opencs/model/world/idtableproxymodel.hpp b/apps/opencs/model/world/idtableproxymodel.hpp index 639cf47287..b7d38f5a2d 100644 --- a/apps/opencs/model/world/idtableproxymodel.hpp +++ b/apps/opencs/model/world/idtableproxymodel.hpp @@ -10,6 +10,9 @@ #include #include #include +#include + +#include "../prefs/state.hpp" #include "columns.hpp" @@ -29,6 +32,8 @@ namespace CSMWorld Q_OBJECT std::shared_ptr mFilter; + std::unique_ptr mFilterTimer; + std::shared_ptr mAwaitingFilter; std::map mColumnMap; // column ID, column index in this model (or -1) // Cache of enum values for enum columns (e.g. Modified, Record Type). @@ -68,6 +73,10 @@ namespace CSMWorld virtual void sourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); + void timerTimeout(); + + void settingChanged(const CSMPrefs::Setting* setting); + signals: void rowAdded(const std::string& id); diff --git a/apps/opencs/model/world/infoselectwrapper.cpp b/apps/opencs/model/world/infoselectwrapper.cpp index fb1adf16d4..2e9ed6e150 100644 --- a/apps/opencs/model/world/infoselectwrapper.cpp +++ b/apps/opencs/model/world/infoselectwrapper.cpp @@ -6,16 +6,9 @@ #include -const size_t CSMWorld::ConstInfoSelectWrapper::RuleMinSize = 5; - -const size_t CSMWorld::ConstInfoSelectWrapper::FunctionPrefixOffset = 1; -const size_t CSMWorld::ConstInfoSelectWrapper::FunctionIndexOffset = 2; -const size_t CSMWorld::ConstInfoSelectWrapper::RelationIndexOffset = 4; -const size_t CSMWorld::ConstInfoSelectWrapper::VarNameOffset = 5; - const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = { - "Rank Low", - "Rank High", + "Faction Reaction Low", + "Faction Reaction High", "Rank Requirement", "Reputation", "Health Percent", @@ -72,7 +65,7 @@ const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = { "PC Endurance", "PC Personality", "PC Luck", - "PC Corpus", + "PC Corprus", "Weather", "PC Vampire", "Level", @@ -112,55 +105,40 @@ const char* CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[] = { nullptr, }; -const char* CSMWorld::ConstInfoSelectWrapper::ComparisonEnumStrings[] = { - "Boolean", - "Integer", - "Numeric", - nullptr, -}; - -// static functions - -std::string CSMWorld::ConstInfoSelectWrapper::convertToString(FunctionName name) +namespace { - if (name < Function_None) - return FunctionEnumStrings[name]; - else + std::string_view convertToString(ESM::DialogueCondition::Function name) + { + if (name < ESM::DialogueCondition::Function_None) + return CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[name]; return "(Invalid Data: Function)"; -} + } -std::string CSMWorld::ConstInfoSelectWrapper::convertToString(RelationType type) -{ - if (type < Relation_None) - return RelationEnumStrings[type]; - else + std::string_view convertToString(ESM::DialogueCondition::Comparison type) + { + if (type != ESM::DialogueCondition::Comp_None) + return CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[type - ESM::DialogueCondition::Comp_Eq]; return "(Invalid Data: Relation)"; -} - -std::string CSMWorld::ConstInfoSelectWrapper::convertToString(ComparisonType type) -{ - if (type < Comparison_None) - return ComparisonEnumStrings[type]; - else - return "(Invalid Data: Comparison)"; + } } // ConstInfoSelectWrapper -CSMWorld::ConstInfoSelectWrapper::ConstInfoSelectWrapper(const ESM::DialInfo::SelectStruct& select) +CSMWorld::ConstInfoSelectWrapper::ConstInfoSelectWrapper(const ESM::DialogueCondition& select) : mConstSelect(select) { - readRule(); + updateHasVariable(); + updateComparisonType(); } -CSMWorld::ConstInfoSelectWrapper::FunctionName CSMWorld::ConstInfoSelectWrapper::getFunctionName() const +ESM::DialogueCondition::Function CSMWorld::ConstInfoSelectWrapper::getFunctionName() const { - return mFunctionName; + return mConstSelect.mFunction; } -CSMWorld::ConstInfoSelectWrapper::RelationType CSMWorld::ConstInfoSelectWrapper::getRelationType() const +ESM::DialogueCondition::Comparison CSMWorld::ConstInfoSelectWrapper::getRelationType() const { - return mRelationType; + return mConstSelect.mComparison; } CSMWorld::ConstInfoSelectWrapper::ComparisonType CSMWorld::ConstInfoSelectWrapper::getComparisonType() const @@ -175,24 +153,21 @@ bool CSMWorld::ConstInfoSelectWrapper::hasVariable() const const std::string& CSMWorld::ConstInfoSelectWrapper::getVariableName() const { - return mVariableName; + return mConstSelect.mVariable; } bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue() const { - if (!variantTypeIsValid()) - return false; - if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidFloatRange()); @@ -203,19 +178,16 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue() const bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const { - if (!variantTypeIsValid()) - return false; - if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsNeverTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsNeverTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidFloatRange()); @@ -224,170 +196,36 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const return false; } -bool CSMWorld::ConstInfoSelectWrapper::variantTypeIsValid() const -{ - return (mConstSelect.mValue.getType() == ESM::VT_Int || mConstSelect.mValue.getType() == ESM::VT_Float); -} - -const ESM::Variant& CSMWorld::ConstInfoSelectWrapper::getVariant() const -{ - return mConstSelect.mValue; -} - std::string CSMWorld::ConstInfoSelectWrapper::toString() const { std::ostringstream stream; - stream << convertToString(mFunctionName) << " "; + stream << convertToString(getFunctionName()) << " "; if (mHasVariable) - stream << mVariableName << " "; + stream << getVariableName() << " "; - stream << convertToString(mRelationType) << " "; + stream << convertToString(getRelationType()) << " "; - switch (mConstSelect.mValue.getType()) - { - case ESM::VT_Int: - stream << mConstSelect.mValue.getInteger(); - break; - - case ESM::VT_Float: - stream << mConstSelect.mValue.getFloat(); - break; - - default: - stream << "(Invalid value type)"; - break; - } + std::visit([&](auto value) { stream << value; }, mConstSelect.mValue); return stream.str(); } -void CSMWorld::ConstInfoSelectWrapper::readRule() -{ - if (mConstSelect.mSelectRule.size() < RuleMinSize) - throw std::runtime_error("InfoSelectWrapper: rule is to small"); - - readFunctionName(); - readRelationType(); - readVariableName(); - updateHasVariable(); - updateComparisonType(); -} - -void CSMWorld::ConstInfoSelectWrapper::readFunctionName() -{ - char functionPrefix = mConstSelect.mSelectRule[FunctionPrefixOffset]; - std::string functionIndex = mConstSelect.mSelectRule.substr(FunctionIndexOffset, 2); - int convertedIndex = -1; - - // Read in function index, form ## from 00 .. 73, skip leading zero - if (functionIndex[0] == '0') - functionIndex = functionIndex[1]; - - std::stringstream stream; - stream << functionIndex; - stream >> convertedIndex; - - switch (functionPrefix) - { - case '1': - if (convertedIndex >= 0 && convertedIndex <= 73) - mFunctionName = static_cast(convertedIndex); - else - mFunctionName = Function_None; - break; - - case '2': - mFunctionName = Function_Global; - break; - case '3': - mFunctionName = Function_Local; - break; - case '4': - mFunctionName = Function_Journal; - break; - case '5': - mFunctionName = Function_Item; - break; - case '6': - mFunctionName = Function_Dead; - break; - case '7': - mFunctionName = Function_NotId; - break; - case '8': - mFunctionName = Function_NotFaction; - break; - case '9': - mFunctionName = Function_NotClass; - break; - case 'A': - mFunctionName = Function_NotRace; - break; - case 'B': - mFunctionName = Function_NotCell; - break; - case 'C': - mFunctionName = Function_NotLocal; - break; - default: - mFunctionName = Function_None; - break; - } -} - -void CSMWorld::ConstInfoSelectWrapper::readRelationType() -{ - char relationIndex = mConstSelect.mSelectRule[RelationIndexOffset]; - - switch (relationIndex) - { - case '0': - mRelationType = Relation_Equal; - break; - case '1': - mRelationType = Relation_NotEqual; - break; - case '2': - mRelationType = Relation_Greater; - break; - case '3': - mRelationType = Relation_GreaterOrEqual; - break; - case '4': - mRelationType = Relation_Less; - break; - case '5': - mRelationType = Relation_LessOrEqual; - break; - default: - mRelationType = Relation_None; - } -} - -void CSMWorld::ConstInfoSelectWrapper::readVariableName() -{ - if (mConstSelect.mSelectRule.size() >= VarNameOffset) - mVariableName = mConstSelect.mSelectRule.substr(VarNameOffset); - else - mVariableName.clear(); -} - void CSMWorld::ConstInfoSelectWrapper::updateHasVariable() { - switch (mFunctionName) + switch (getFunctionName()) { - case Function_Global: - case Function_Local: - case Function_Journal: - case Function_Item: - case Function_Dead: - case Function_NotId: - case Function_NotFaction: - case Function_NotClass: - case Function_NotRace: - case Function_NotCell: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_Journal: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: + case ESM::DialogueCondition::Function_NotLocal: mHasVariable = true; break; @@ -399,103 +237,103 @@ void CSMWorld::ConstInfoSelectWrapper::updateHasVariable() void CSMWorld::ConstInfoSelectWrapper::updateComparisonType() { - switch (mFunctionName) + switch (getFunctionName()) { // Boolean - case Function_NotId: - case Function_NotFaction: - case Function_NotClass: - case Function_NotRace: - case Function_NotCell: - case Function_PcExpelled: - case Function_PcCommonDisease: - case Function_PcBlightDisease: - case Function_SameSex: - case Function_SameRace: - case Function_SameFaction: - case Function_Detected: - case Function_Alarmed: - case Function_PcCorpus: - case Function_PcVampire: - case Function_Attacked: - case Function_TalkedToPc: - case Function_ShouldAttack: - case Function_Werewolf: + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: + case ESM::DialogueCondition::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_Detected: + case ESM::DialogueCondition::Function_Alarmed: + case ESM::DialogueCondition::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcVampire: + case ESM::DialogueCondition::Function_Attacked: + case ESM::DialogueCondition::Function_TalkedToPc: + case ESM::DialogueCondition::Function_ShouldAttack: + case ESM::DialogueCondition::Function_Werewolf: mComparisonType = Comparison_Boolean; break; // Integer - case Function_Journal: - case Function_Item: - case Function_Dead: - case Function_RankLow: - case Function_RankHigh: - case Function_RankRequirement: - case Function_Reputation: - case Function_PcReputation: - case Function_PcLevel: - case Function_PcStrength: - case Function_PcBlock: - case Function_PcArmorer: - case Function_PcMediumArmor: - case Function_PcHeavyArmor: - case Function_PcBluntWeapon: - case Function_PcLongBlade: - case Function_PcAxe: - case Function_PcSpear: - case Function_PcAthletics: - case Function_PcEnchant: - case Function_PcDestruction: - case Function_PcAlteration: - case Function_PcIllusion: - case Function_PcConjuration: - case Function_PcMysticism: - case Function_PcRestoration: - case Function_PcAlchemy: - case Function_PcUnarmored: - case Function_PcSecurity: - case Function_PcSneak: - case Function_PcAcrobatics: - case Function_PcLightArmor: - case Function_PcShortBlade: - case Function_PcMarksman: - case Function_PcMerchantile: - case Function_PcSpeechcraft: - case Function_PcHandToHand: - case Function_PcGender: - case Function_PcClothingModifier: - case Function_PcCrimeLevel: - case Function_FactionRankDifference: - case Function_Choice: - case Function_PcIntelligence: - case Function_PcWillpower: - case Function_PcAgility: - case Function_PcSpeed: - case Function_PcEndurance: - case Function_PcPersonality: - case Function_PcLuck: - case Function_Weather: - case Function_Level: - case Function_CreatureTarget: - case Function_FriendHit: - case Function_Fight: - case Function_Hello: - case Function_Alarm: - case Function_Flee: - case Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_Journal: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: + case ESM::DialogueCondition::Function_RankRequirement: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_PcReputation: + case ESM::DialogueCondition::Function_PcLevel: + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + case ESM::DialogueCondition::Function_PcGender: + case ESM::DialogueCondition::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_FactionRankDifference: + case ESM::DialogueCondition::Function_Choice: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: + case ESM::DialogueCondition::Function_Weather: + case ESM::DialogueCondition::Function_Level: + case ESM::DialogueCondition::Function_CreatureTarget: + case ESM::DialogueCondition::Function_FriendHit: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: + case ESM::DialogueCondition::Function_PcWerewolfKills: mComparisonType = Comparison_Integer; break; // Numeric - case Function_Global: - case Function_Local: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: - case Function_Health_Percent: - case Function_PcHealthPercent: - case Function_PcMagicka: - case Function_PcFatigue: - case Function_PcHealth: + case ESM::DialogueCondition::Function_Health_Percent: + case ESM::DialogueCondition::Function_PcHealthPercent: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: mComparisonType = Comparison_Numeric; break; @@ -511,15 +349,15 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() con const int IntMin = std::numeric_limits::min(); const std::pair InvalidRange(IntMax, IntMin); - int value = mConstSelect.mValue.getInteger(); + int value = std::get(mConstSelect.mValue); - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Eq: + case ESM::DialogueCondition::Comp_Ne: return std::pair(value, value); - case Relation_Greater: + case ESM::DialogueCondition::Comp_Gt: if (value == IntMax) { return InvalidRange; @@ -530,10 +368,10 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() con } break; - case Relation_GreaterOrEqual: + case ESM::DialogueCondition::Comp_Ge: return std::pair(value, IntMax); - case Relation_Less: + case ESM::DialogueCondition::Comp_Ls: if (value == IntMin) { return InvalidRange; @@ -543,7 +381,7 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() con return std::pair(IntMin, value - 1); } - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Le: return std::pair(IntMin, value); default: @@ -557,24 +395,24 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionFloatRange const float FloatMin = -std::numeric_limits::infinity(); const float Epsilon = std::numeric_limits::epsilon(); - float value = mConstSelect.mValue.getFloat(); + float value = std::get(mConstSelect.mValue); - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Eq: + case ESM::DialogueCondition::Comp_Ne: return std::pair(value, value); - case Relation_Greater: + case ESM::DialogueCondition::Comp_Gt: return std::pair(value + Epsilon, FloatMax); - case Relation_GreaterOrEqual: + case ESM::DialogueCondition::Comp_Ge: return std::pair(value, FloatMax); - case Relation_Less: + case ESM::DialogueCondition::Comp_Ls: return std::pair(FloatMin, value - Epsilon); - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Le: return std::pair(FloatMin, value); default: @@ -587,120 +425,120 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getValidIntRange() const const int IntMax = std::numeric_limits::max(); const int IntMin = std::numeric_limits::min(); - switch (mFunctionName) + switch (getFunctionName()) { // Boolean - case Function_NotId: - case Function_NotFaction: - case Function_NotClass: - case Function_NotRace: - case Function_NotCell: - case Function_PcExpelled: - case Function_PcCommonDisease: - case Function_PcBlightDisease: - case Function_SameSex: - case Function_SameRace: - case Function_SameFaction: - case Function_Detected: - case Function_Alarmed: - case Function_PcCorpus: - case Function_PcVampire: - case Function_Attacked: - case Function_TalkedToPc: - case Function_ShouldAttack: - case Function_Werewolf: + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: + case ESM::DialogueCondition::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_Detected: + case ESM::DialogueCondition::Function_Alarmed: + case ESM::DialogueCondition::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcVampire: + case ESM::DialogueCondition::Function_Attacked: + case ESM::DialogueCondition::Function_TalkedToPc: + case ESM::DialogueCondition::Function_ShouldAttack: + case ESM::DialogueCondition::Function_Werewolf: return std::pair(0, 1); // Integer - case Function_RankLow: - case Function_RankHigh: - case Function_Reputation: - case Function_PcReputation: - case Function_Journal: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_PcReputation: + case ESM::DialogueCondition::Function_Journal: return std::pair(IntMin, IntMax); - case Function_Item: - case Function_Dead: - case Function_PcLevel: - case Function_PcStrength: - case Function_PcBlock: - case Function_PcArmorer: - case Function_PcMediumArmor: - case Function_PcHeavyArmor: - case Function_PcBluntWeapon: - case Function_PcLongBlade: - case Function_PcAxe: - case Function_PcSpear: - case Function_PcAthletics: - case Function_PcEnchant: - case Function_PcDestruction: - case Function_PcAlteration: - case Function_PcIllusion: - case Function_PcConjuration: - case Function_PcMysticism: - case Function_PcRestoration: - case Function_PcAlchemy: - case Function_PcUnarmored: - case Function_PcSecurity: - case Function_PcSneak: - case Function_PcAcrobatics: - case Function_PcLightArmor: - case Function_PcShortBlade: - case Function_PcMarksman: - case Function_PcMerchantile: - case Function_PcSpeechcraft: - case Function_PcHandToHand: - case Function_PcClothingModifier: - case Function_PcCrimeLevel: - case Function_Choice: - case Function_PcIntelligence: - case Function_PcWillpower: - case Function_PcAgility: - case Function_PcSpeed: - case Function_PcEndurance: - case Function_PcPersonality: - case Function_PcLuck: - case Function_Level: - case Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_PcLevel: + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + case ESM::DialogueCondition::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_Choice: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: + case ESM::DialogueCondition::Function_Level: + case ESM::DialogueCondition::Function_PcWerewolfKills: return std::pair(0, IntMax); - case Function_Fight: - case Function_Hello: - case Function_Alarm: - case Function_Flee: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: return std::pair(0, 100); - case Function_Weather: + case ESM::DialogueCondition::Function_Weather: return std::pair(0, 9); - case Function_FriendHit: + case ESM::DialogueCondition::Function_FriendHit: return std::pair(0, 4); - case Function_RankRequirement: + case ESM::DialogueCondition::Function_RankRequirement: return std::pair(0, 3); - case Function_CreatureTarget: + case ESM::DialogueCondition::Function_CreatureTarget: return std::pair(0, 2); - case Function_PcGender: + case ESM::DialogueCondition::Function_PcGender: return std::pair(0, 1); - case Function_FactionRankDifference: + case ESM::DialogueCondition::Function_FactionRankDifference: return std::pair(-9, 9); // Numeric - case Function_Global: - case Function_Local: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: return std::pair(IntMin, IntMax); - case Function_PcMagicka: - case Function_PcFatigue: - case Function_PcHealth: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: return std::pair(0, IntMax); - case Function_Health_Percent: - case Function_PcHealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: + case ESM::DialogueCondition::Function_PcHealthPercent: return std::pair(0, 100); default: @@ -713,21 +551,21 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getValidFloatRange() c const float FloatMax = std::numeric_limits::infinity(); const float FloatMin = -std::numeric_limits::infinity(); - switch (mFunctionName) + switch (getFunctionName()) { // Numeric - case Function_Global: - case Function_Local: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: return std::pair(FloatMin, FloatMax); - case Function_PcMagicka: - case Function_PcFatigue: - case Function_PcHealth: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: return std::pair(0, FloatMax); - case Function_Health_Percent: - case Function_PcHealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: + case ESM::DialogueCondition::Function_PcHealthPercent: return std::pair(0, 100); default: @@ -768,19 +606,19 @@ template bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue( std::pair conditionRange, std::pair validRange) const { - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: + case ESM::DialogueCondition::Comp_Eq: return false; - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Ne: // If value is not within range, it will always be true return !rangeContains(conditionRange.first, validRange); - case Relation_Greater: - case Relation_GreaterOrEqual: - case Relation_Less: - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Gt: + case ESM::DialogueCondition::Comp_Ge: + case ESM::DialogueCondition::Comp_Ls: + case ESM::DialogueCondition::Comp_Le: // If the valid range is completely within the condition range, it will always be true return rangeFullyContains(conditionRange, validRange); @@ -795,18 +633,18 @@ template bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue( std::pair conditionRange, std::pair validRange) const { - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: + case ESM::DialogueCondition::Comp_Eq: return !rangeContains(conditionRange.first, validRange); - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Ne: return false; - case Relation_Greater: - case Relation_GreaterOrEqual: - case Relation_Less: - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Gt: + case ESM::DialogueCondition::Comp_Ge: + case ESM::DialogueCondition::Comp_Ls: + case ESM::DialogueCondition::Comp_Le: // If ranges do not overlap, it will never be true return !rangesOverlap(conditionRange, validRange); @@ -817,153 +655,47 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue( return false; } +QVariant CSMWorld::ConstInfoSelectWrapper::getValue() const +{ + return std::visit([](auto value) { return QVariant(value); }, mConstSelect.mValue); +} + // InfoSelectWrapper -CSMWorld::InfoSelectWrapper::InfoSelectWrapper(ESM::DialInfo::SelectStruct& select) +CSMWorld::InfoSelectWrapper::InfoSelectWrapper(ESM::DialogueCondition& select) : CSMWorld::ConstInfoSelectWrapper(select) , mSelect(select) { } -void CSMWorld::InfoSelectWrapper::setFunctionName(FunctionName name) +void CSMWorld::InfoSelectWrapper::setFunctionName(ESM::DialogueCondition::Function name) { - mFunctionName = name; + mSelect.mFunction = name; updateHasVariable(); updateComparisonType(); + if (getComparisonType() != ConstInfoSelectWrapper::Comparison_Numeric + && std::holds_alternative(mSelect.mValue)) + { + mSelect.mValue = std::visit([](auto value) { return static_cast(value); }, mSelect.mValue); + } } -void CSMWorld::InfoSelectWrapper::setRelationType(RelationType type) +void CSMWorld::InfoSelectWrapper::setRelationType(ESM::DialogueCondition::Comparison type) { - mRelationType = type; + mSelect.mComparison = type; } void CSMWorld::InfoSelectWrapper::setVariableName(const std::string& name) { - mVariableName = name; + mSelect.mVariable = name; } -void CSMWorld::InfoSelectWrapper::setDefaults() +void CSMWorld::InfoSelectWrapper::setValue(int value) { - if (!variantTypeIsValid()) - mSelect.mValue.setType(ESM::VT_Int); - - switch (mComparisonType) - { - case Comparison_Boolean: - setRelationType(Relation_Equal); - mSelect.mValue.setInteger(1); - break; - - case Comparison_Integer: - case Comparison_Numeric: - setRelationType(Relation_Greater); - mSelect.mValue.setInteger(0); - break; - - default: - // Do nothing - break; - } - - update(); + mSelect.mValue = value; } -void CSMWorld::InfoSelectWrapper::update() +void CSMWorld::InfoSelectWrapper::setValue(float value) { - std::ostringstream stream; - - // Leading 0 - stream << '0'; - - // Write Function - - bool writeIndex = false; - size_t functionIndex = static_cast(mFunctionName); - - switch (mFunctionName) - { - case Function_None: - stream << '0'; - break; - case Function_Global: - stream << '2'; - break; - case Function_Local: - stream << '3'; - break; - case Function_Journal: - stream << '4'; - break; - case Function_Item: - stream << '5'; - break; - case Function_Dead: - stream << '6'; - break; - case Function_NotId: - stream << '7'; - break; - case Function_NotFaction: - stream << '8'; - break; - case Function_NotClass: - stream << '9'; - break; - case Function_NotRace: - stream << 'A'; - break; - case Function_NotCell: - stream << 'B'; - break; - case Function_NotLocal: - stream << 'C'; - break; - default: - stream << '1'; - writeIndex = true; - break; - } - - if (writeIndex && functionIndex < 10) // leading 0 - stream << '0' << functionIndex; - else if (writeIndex) - stream << functionIndex; - else - stream << "00"; - - // Write Relation - switch (mRelationType) - { - case Relation_Equal: - stream << '0'; - break; - case Relation_NotEqual: - stream << '1'; - break; - case Relation_Greater: - stream << '2'; - break; - case Relation_GreaterOrEqual: - stream << '3'; - break; - case Relation_Less: - stream << '4'; - break; - case Relation_LessOrEqual: - stream << '5'; - break; - default: - stream << '0'; - break; - } - - if (mHasVariable) - stream << mVariableName; - - mSelect.mSelectRule = stream.str(); -} - -ESM::Variant& CSMWorld::InfoSelectWrapper::getVariant() -{ - return mSelect.mValue; + mSelect.mValue = value; } diff --git a/apps/opencs/model/world/infoselectwrapper.hpp b/apps/opencs/model/world/infoselectwrapper.hpp index 4ecdc676dc..b3b5abe462 100644 --- a/apps/opencs/model/world/infoselectwrapper.hpp +++ b/apps/opencs/model/world/infoselectwrapper.hpp @@ -7,133 +7,13 @@ #include -namespace ESM -{ - class Variant; -} +#include namespace CSMWorld { - // ESM::DialInfo::SelectStruct.mSelectRule - // 012345... - // ^^^ ^^ - // ||| || - // ||| |+------------- condition variable string - // ||| +-------------- comparison type, ['0'..'5']; e.g. !=, <, >=, etc - // ||+---------------- function index (encoded, where function == '1') - // |+----------------- function, ['1'..'C']; e.g. Global, Local, Not ID, etc - // +------------------ unknown - // - - // Wrapper for DialInfo::SelectStruct class ConstInfoSelectWrapper { public: - // Order matters - enum FunctionName - { - Function_RankLow = 0, - Function_RankHigh, - Function_RankRequirement, - Function_Reputation, - Function_Health_Percent, - Function_PcReputation, - Function_PcLevel, - Function_PcHealthPercent, - Function_PcMagicka, - Function_PcFatigue, - Function_PcStrength, - Function_PcBlock, - Function_PcArmorer, - Function_PcMediumArmor, - Function_PcHeavyArmor, - Function_PcBluntWeapon, - Function_PcLongBlade, - Function_PcAxe, - Function_PcSpear, - Function_PcAthletics, - Function_PcEnchant, - Function_PcDestruction, - Function_PcAlteration, - Function_PcIllusion, - Function_PcConjuration, - Function_PcMysticism, - Function_PcRestoration, - Function_PcAlchemy, - Function_PcUnarmored, - Function_PcSecurity, - Function_PcSneak, - Function_PcAcrobatics, - Function_PcLightArmor, - Function_PcShortBlade, - Function_PcMarksman, - Function_PcMerchantile, - Function_PcSpeechcraft, - Function_PcHandToHand, - Function_PcGender, - Function_PcExpelled, - Function_PcCommonDisease, - Function_PcBlightDisease, - Function_PcClothingModifier, - Function_PcCrimeLevel, - Function_SameSex, - Function_SameRace, - Function_SameFaction, - Function_FactionRankDifference, - Function_Detected, - Function_Alarmed, - Function_Choice, - Function_PcIntelligence, - Function_PcWillpower, - Function_PcAgility, - Function_PcSpeed, - Function_PcEndurance, - Function_PcPersonality, - Function_PcLuck, - Function_PcCorpus, - Function_Weather, - Function_PcVampire, - Function_Level, - Function_Attacked, - Function_TalkedToPc, - Function_PcHealth, - Function_CreatureTarget, - Function_FriendHit, - Function_Fight, - Function_Hello, - Function_Alarm, - Function_Flee, - Function_ShouldAttack, - Function_Werewolf, - Function_PcWerewolfKills = 73, - - Function_Global, - Function_Local, - Function_Journal, - Function_Item, - Function_Dead, - Function_NotId, - Function_NotFaction, - Function_NotClass, - Function_NotRace, - Function_NotCell, - Function_NotLocal, - - Function_None - }; - - enum RelationType - { - Relation_Equal, - Relation_NotEqual, - Relation_Greater, - Relation_GreaterOrEqual, - Relation_Less, - Relation_LessOrEqual, - - Relation_None - }; - enum ComparisonType { Comparison_Boolean, @@ -143,25 +23,13 @@ namespace CSMWorld Comparison_None }; - static const size_t RuleMinSize; - - static const size_t FunctionPrefixOffset; - static const size_t FunctionIndexOffset; - static const size_t RelationIndexOffset; - static const size_t VarNameOffset; - static const char* FunctionEnumStrings[]; static const char* RelationEnumStrings[]; - static const char* ComparisonEnumStrings[]; - static std::string convertToString(FunctionName name); - static std::string convertToString(RelationType type); - static std::string convertToString(ComparisonType type); + ConstInfoSelectWrapper(const ESM::DialogueCondition& select); - ConstInfoSelectWrapper(const ESM::DialInfo::SelectStruct& select); - - FunctionName getFunctionName() const; - RelationType getRelationType() const; + ESM::DialogueCondition::Function getFunctionName() const; + ESM::DialogueCondition::Comparison getRelationType() const; ComparisonType getComparisonType() const; bool hasVariable() const; @@ -169,17 +37,12 @@ namespace CSMWorld bool conditionIsAlwaysTrue() const; bool conditionIsNeverTrue() const; - bool variantTypeIsValid() const; - const ESM::Variant& getVariant() const; + QVariant getValue() const; std::string toString() const; protected: - void readRule(); - void readFunctionName(); - void readRelationType(); - void readVariableName(); void updateHasVariable(); void updateComparisonType(); @@ -207,38 +70,29 @@ namespace CSMWorld template bool conditionIsNeverTrue(std::pair conditionRange, std::pair validRange) const; - FunctionName mFunctionName; - RelationType mRelationType; ComparisonType mComparisonType; bool mHasVariable; - std::string mVariableName; private: - const ESM::DialInfo::SelectStruct& mConstSelect; + const ESM::DialogueCondition& mConstSelect; }; - // Wrapper for DialInfo::SelectStruct that can modify the wrapped select struct + // Wrapper for DialogueCondition that can modify the wrapped select struct class InfoSelectWrapper : public ConstInfoSelectWrapper { public: - InfoSelectWrapper(ESM::DialInfo::SelectStruct& select); + InfoSelectWrapper(ESM::DialogueCondition& select); // Wrapped SelectStruct will not be modified until update() is called - void setFunctionName(FunctionName name); - void setRelationType(RelationType type); + void setFunctionName(ESM::DialogueCondition::Function name); + void setRelationType(ESM::DialogueCondition::Comparison type); void setVariableName(const std::string& name); - - // Modified wrapped SelectStruct - void update(); - - // This sets properties based on the function name to its defaults and updates the wrapped object - void setDefaults(); - - ESM::Variant& getVariant(); + void setValue(int value); + void setValue(float value); private: - ESM::DialInfo::SelectStruct& mSelect; + ESM::DialogueCondition& mSelect; void writeRule(); }; diff --git a/apps/opencs/model/world/landtexture.cpp b/apps/opencs/model/world/landtexture.cpp deleted file mode 100644 index ecf370c282..0000000000 --- a/apps/opencs/model/world/landtexture.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "landtexture.hpp" - -#include -#include - -#include -#include - -namespace CSMWorld -{ - void LandTexture::load(ESM::ESMReader& esm, bool& isDeleted) - { - ESM::LandTexture::load(esm, isDeleted); - - mPluginIndex = esm.getIndex(); - } - - std::string LandTexture::createUniqueRecordId(int plugin, int index) - { - std::stringstream ss; - ss << 'L' << plugin << '#' << index; - return ss.str(); - } - - void LandTexture::parseUniqueRecordId(const std::string& id, int& plugin, int& index) - { - size_t middle = id.find('#'); - - if (middle == std::string::npos || id[0] != 'L') - throw std::runtime_error("Invalid LandTexture ID"); - - plugin = Misc::StringUtils::toNumeric(id.substr(1, middle - 1), 0); - index = Misc::StringUtils::toNumeric(id.substr(middle + 1), 0); - } -} diff --git a/apps/opencs/model/world/landtexture.hpp b/apps/opencs/model/world/landtexture.hpp deleted file mode 100644 index d3cac5d3d7..0000000000 --- a/apps/opencs/model/world/landtexture.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef CSM_WORLD_LANDTEXTURE_H -#define CSM_WORLD_LANDTEXTURE_H - -#include - -#include - -namespace ESM -{ - class ESMReader; -} - -namespace CSMWorld -{ - /// \brief Wrapper for LandTexture record, providing info which plugin the LandTexture was loaded from. - struct LandTexture : public ESM::LandTexture - { - int mPluginIndex; - - void load(ESM::ESMReader& esm, bool& isDeleted); - - /// Returns a string identifier that will be unique to any LandTexture. - static std::string createUniqueRecordId(int plugin, int index); - /// Deconstructs a unique string identifier into plugin and index. - static void parseUniqueRecordId(const std::string& id, int& plugin, int& index); - }; -} - -#endif diff --git a/apps/opencs/model/world/nestedcoladapterimp.cpp b/apps/opencs/model/world/nestedcoladapterimp.cpp index 9572d8de39..26c52ce50a 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.cpp +++ b/apps/opencs/model/world/nestedcoladapterimp.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -38,10 +39,9 @@ namespace CSMWorld point.mZ = 0; point.mAutogenerated = 0; point.mConnectionNum = 0; - point.mUnknown = 0; points.insert(points.begin() + position, point); - pathgrid.mData.mS2 = pathgrid.mPoints.size(); + pathgrid.mData.mPoints = pathgrid.mPoints.size(); record.setModified(pathgrid); } @@ -58,7 +58,7 @@ namespace CSMWorld // Do not remove dangling edges, does not work with current undo mechanism // Do not automatically adjust indices, what would be done with dangling edges? points.erase(points.begin() + rowToRemove); - pathgrid.mData.mS2 = pathgrid.mPoints.size(); + pathgrid.mData.mPoints = pathgrid.mPoints.size(); record.setModified(pathgrid); } @@ -67,7 +67,7 @@ namespace CSMWorld { Pathgrid pathgrid = record.get(); pathgrid.mPoints = static_cast&>(nestedTable).mNestedTable; - pathgrid.mData.mS2 = pathgrid.mPoints.size(); + pathgrid.mData.mPoints = pathgrid.mPoints.size(); record.setModified(pathgrid); } @@ -414,20 +414,32 @@ namespace CSMWorld QVariant RegionSoundListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { - ESM::Region region = record.get(); + const ESM::Region& region = record.get(); - std::vector& soundList = region.mSoundList; + const std::vector& soundList = region.mSoundList; - if (subRowIndex < 0 || subRowIndex >= static_cast(soundList.size())) + const size_t index = static_cast(subRowIndex); + if (subRowIndex < 0 || index >= soundList.size()) throw std::runtime_error("index out of range"); - ESM::Region::SoundRef soundRef = soundList[subRowIndex]; + const ESM::Region::SoundRef& soundRef = soundList[subRowIndex]; switch (subColIndex) { case 0: return QString(soundRef.mSound.getRefIdString().c_str()); case 1: return soundRef.mChance; + case 2: + { + float probability = 1.f; + for (size_t i = 0; i < index; ++i) + { + const float p = std::min(soundList[i].mChance / 100.f, 1.f); + probability *= 1.f - p; + } + probability *= std::min(soundRef.mChance / 100.f, 1.f) * 100.f; + return QString("%1%").arg(probability, 0, 'f', 2); + } default: throw std::runtime_error("Region sounds subcolumn index out of range"); } @@ -463,7 +475,7 @@ namespace CSMWorld int RegionSoundListAdapter::getColumnsCount(const Record& record) const { - return 2; + return 3; } int RegionSoundListAdapter::getRowsCount(const Record& record) const @@ -527,13 +539,11 @@ namespace CSMWorld { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; // default row - ESM::DialInfo::SelectStruct condStruct; - condStruct.mSelectRule = "01000"; - condStruct.mValue = ESM::Variant(); - condStruct.mValue.setType(ESM::VT_Int); + ESM::DialogueCondition condStruct; + condStruct.mIndex = conditions.size(); conditions.insert(conditions.begin() + position, condStruct); @@ -544,7 +554,7 @@ namespace CSMWorld { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; if (rowToRemove < 0 || rowToRemove >= static_cast(conditions.size())) throw std::runtime_error("index out of range"); @@ -558,8 +568,8 @@ namespace CSMWorld { Info info = record.get(); - info.mSelects = static_cast>&>(nestedTable) - .mNestedTable; + info.mSelects + = static_cast>&>(nestedTable).mNestedTable; record.setModified(info); } @@ -567,14 +577,14 @@ namespace CSMWorld NestedTableWrapperBase* InfoConditionAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper>(record.get().mSelects); + return new NestedTableWrapper>(record.get().mSelects); } QVariant InfoConditionAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; if (subRowIndex < 0 || subRowIndex >= static_cast(conditions.size())) throw std::runtime_error("index out of range"); @@ -596,23 +606,11 @@ namespace CSMWorld } case 2: { - return infoSelectWrapper.getRelationType(); + return infoSelectWrapper.getRelationType() - ESM::DialogueCondition::Comp_Eq; } case 3: { - switch (infoSelectWrapper.getVariant().getType()) - { - case ESM::VT_Int: - { - return infoSelectWrapper.getVariant().getInteger(); - } - case ESM::VT_Float: - { - return infoSelectWrapper.getVariant().getFloat(); - } - default: - return QVariant(); - } + return infoSelectWrapper.getValue(); } default: throw std::runtime_error("Info condition subcolumn index out of range"); @@ -624,7 +622,7 @@ namespace CSMWorld { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; if (subRowIndex < 0 || subRowIndex >= static_cast(conditions.size())) throw std::runtime_error("index out of range"); @@ -636,27 +634,18 @@ namespace CSMWorld { case 0: // Function { - infoSelectWrapper.setFunctionName(static_cast(value.toInt())); - - if (infoSelectWrapper.getComparisonType() != ConstInfoSelectWrapper::Comparison_Numeric - && infoSelectWrapper.getVariant().getType() != ESM::VT_Int) - { - infoSelectWrapper.getVariant().setType(ESM::VT_Int); - } - - infoSelectWrapper.update(); + infoSelectWrapper.setFunctionName(static_cast(value.toInt())); break; } case 1: // Variable { infoSelectWrapper.setVariableName(value.toString().toUtf8().constData()); - infoSelectWrapper.update(); break; } case 2: // Relation { - infoSelectWrapper.setRelationType(static_cast(value.toInt())); - infoSelectWrapper.update(); + infoSelectWrapper.setRelationType( + static_cast(value.toInt() + ESM::DialogueCondition::Comp_Eq)); break; } case 3: // Value @@ -668,13 +657,11 @@ namespace CSMWorld // QVariant seems to have issues converting 0 if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { - infoSelectWrapper.getVariant().setType(ESM::VT_Int); - infoSelectWrapper.getVariant().setInteger(value.toInt()); + infoSelectWrapper.setValue(value.toInt()); } else if (value.toFloat(&conversionResult) && conversionResult) { - infoSelectWrapper.getVariant().setType(ESM::VT_Float); - infoSelectWrapper.getVariant().setFloat(value.toFloat()); + infoSelectWrapper.setValue(value.toFloat()); } break; } @@ -683,8 +670,7 @@ namespace CSMWorld { if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { - infoSelectWrapper.getVariant().setType(ESM::VT_Int); - infoSelectWrapper.getVariant().setInteger(value.toInt()); + infoSelectWrapper.setValue(value.toInt()); } break; } @@ -741,8 +727,8 @@ namespace CSMWorld QVariant RaceAttributeAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - - if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) + ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex); + if (attribute.empty()) throw std::runtime_error("index out of range"); switch (subColIndex) @@ -750,9 +736,9 @@ namespace CSMWorld case 0: return subRowIndex; case 1: - return race.mData.mAttributeValues[subRowIndex].mMale; + return race.mData.getAttribute(attribute, true); case 2: - return race.mData.mAttributeValues[subRowIndex].mFemale; + return race.mData.getAttribute(attribute, false); default: throw std::runtime_error("Race Attribute subcolumn index out of range"); } @@ -762,8 +748,8 @@ namespace CSMWorld Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - - if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) + ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex); + if (attribute.empty()) throw std::runtime_error("index out of range"); switch (subColIndex) @@ -771,10 +757,10 @@ namespace CSMWorld case 0: return; // throw an exception here? case 1: - race.mData.mAttributeValues[subRowIndex].mMale = value.toInt(); + race.mData.setAttribute(attribute, true, value.toInt()); break; case 2: - race.mData.mAttributeValues[subRowIndex].mFemale = value.toInt(); + race.mData.setAttribute(attribute, false, value.toInt()); break; default: throw std::runtime_error("Race Attribute subcolumn index out of range"); @@ -907,24 +893,24 @@ namespace CSMWorld // While the ambient information is not necessarily valid if the subrecord wasn't loaded, // the user should still be allowed to edit it case 1: - return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mAmbient : QVariant(QVariant::UserType); + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mAmbient : DisableTag::getVariant(); case 2: - return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mSunlight : QVariant(QVariant::UserType); + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mSunlight : DisableTag::getVariant(); case 3: - return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFog : QVariant(QVariant::UserType); + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFog : DisableTag::getVariant(); case 4: - return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFogDensity : QVariant(QVariant::UserType); + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFogDensity : DisableTag::getVariant(); case 5: { if (isInterior && interiorWater) return cell.mWater; else - return QVariant(QVariant::UserType); + return DisableTag::getVariant(); } case 6: - return isInterior ? QVariant(QVariant::UserType) : cell.mMapColor; // TODO: how to select? + return isInterior ? DisableTag::getVariant() : cell.mMapColor; // TODO: how to select? // case 7: return isInterior ? - // behaveLikeExterior : QVariant(QVariant::UserType); + // behaveLikeExterior : DisableTag::getVariant(); default: throw std::runtime_error("Cell subcolumn index out of range"); } @@ -996,7 +982,10 @@ namespace CSMWorld case 5: { if (isInterior && interiorWater) + { cell.mWater = value.toFloat(); + cell.setHasWaterHeightSub(true); + } else return; // return without saving break; @@ -1076,31 +1065,8 @@ namespace CSMWorld } else if (subColIndex == 1) { - switch (subRowIndex) - { - case 0: - return region.mData.mClear; - case 1: - return region.mData.mCloudy; - case 2: - return region.mData.mFoggy; - case 3: - return region.mData.mOvercast; - case 4: - return region.mData.mRain; - case 5: - return region.mData.mThunder; - case 6: - return region.mData.mAsh; - case 7: - return region.mData.mBlight; - case 8: - return region.mData.mSnow; - case 9: - return region.mData.mBlizzard; - default: - break; - } + if (subRowIndex >= 0 && static_cast(subRowIndex) < region.mData.mProbabilities.size()) + return region.mData.mProbabilities[subRowIndex]; } throw std::runtime_error("index out of range"); @@ -1110,45 +1076,11 @@ namespace CSMWorld Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Region region = record.get(); - unsigned char chance = static_cast(value.toInt()); + uint8_t chance = static_cast(value.toInt()); if (subColIndex == 1) { - switch (subRowIndex) - { - case 0: - region.mData.mClear = chance; - break; - case 1: - region.mData.mCloudy = chance; - break; - case 2: - region.mData.mFoggy = chance; - break; - case 3: - region.mData.mOvercast = chance; - break; - case 4: - region.mData.mRain = chance; - break; - case 5: - region.mData.mThunder = chance; - break; - case 6: - region.mData.mAsh = chance; - break; - case 7: - region.mData.mBlight = chance; - break; - case 8: - region.mData.mSnow = chance; - break; - case 9: - region.mData.mBlizzard = chance; - break; - default: - throw std::runtime_error("index out of range"); - } + region.mData.mProbabilities.at(subRowIndex) = chance; record.setModified(region); } diff --git a/apps/opencs/model/world/nestedcoladapterimp.hpp b/apps/opencs/model/world/nestedcoladapterimp.hpp index 235396c650..f5c5501889 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.hpp +++ b/apps/opencs/model/world/nestedcoladapterimp.hpp @@ -255,20 +255,22 @@ namespace CSMWorld { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; // blank row - ESM::ENAMstruct effect; - effect.mEffectID = 0; - effect.mSkill = -1; - effect.mAttribute = -1; - effect.mRange = 0; - effect.mArea = 0; - effect.mDuration = 0; - effect.mMagnMin = 0; - effect.mMagnMax = 0; + ESM::IndexedENAMstruct effect; + effect.mIndex = position; + effect.mData.mEffectID = 0; + effect.mData.mSkill = -1; + effect.mData.mAttribute = -1; + effect.mData.mRange = 0; + effect.mData.mArea = 0; + effect.mData.mDuration = 0; + effect.mData.mMagnMin = 0; + effect.mData.mMagnMax = 0; effectsList.insert(effectsList.begin() + position, effect); + magic.mEffects.updateIndexes(); record.setModified(magic); } @@ -277,12 +279,13 @@ namespace CSMWorld { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; if (rowToRemove < 0 || rowToRemove >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); effectsList.erase(effectsList.begin() + rowToRemove); + magic.mEffects.updateIndexes(); record.setModified(magic); } @@ -292,7 +295,7 @@ namespace CSMWorld ESXRecordT magic = record.get(); magic.mEffects.mList - = static_cast>&>(nestedTable).mNestedTable; + = static_cast>&>(nestedTable).mNestedTable; record.setModified(magic); } @@ -300,28 +303,23 @@ namespace CSMWorld NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper>(record.get().mEffects.mList); + return new NestedTableWrapper>(record.get().mEffects.mList); } QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); - ESM::ENAMstruct effect = effectsList[subRowIndex]; + ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: - { - if (effect.mEffectID >= 0 && effect.mEffectID < ESM::MagicEffect::Length) - return effect.mEffectID; - else - throw std::runtime_error("Magic effects ID unexpected value"); - } + return effect.mEffectID; case 1: { switch (effect.mEffectID) @@ -351,12 +349,7 @@ namespace CSMWorld } } case 3: - { - if (effect.mRange >= 0 && effect.mRange <= 2) - return effect.mRange; - else - throw std::runtime_error("Magic effects range unexpected value"); - } + return effect.mRange; case 4: return effect.mArea; case 5: @@ -374,17 +367,37 @@ namespace CSMWorld { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); - ESM::ENAMstruct effect = effectsList[subRowIndex]; + ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: { effect.mEffectID = static_cast(value.toInt()); + switch (effect.mEffectID) + { + case ESM::MagicEffect::DrainSkill: + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::RestoreSkill: + case ESM::MagicEffect::FortifySkill: + case ESM::MagicEffect::AbsorbSkill: + effect.mAttribute = -1; + break; + case ESM::MagicEffect::DrainAttribute: + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::RestoreAttribute: + case ESM::MagicEffect::FortifyAttribute: + case ESM::MagicEffect::AbsorbAttribute: + effect.mSkill = -1; + break; + default: + effect.mSkill = -1; + effect.mAttribute = -1; + } break; } case 1: @@ -418,7 +431,7 @@ namespace CSMWorld throw std::runtime_error("Magic Effects subcolumn index out of range"); } - magic.mEffects.mList[subRowIndex] = effect; + magic.mEffects.mList[subRowIndex].mData = effect; record.setModified(magic); } diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index d1f64fbfef..35e4c82a35 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -19,6 +19,11 @@ namespace CSMWorld State mState; + explicit RecordBase(State state) + : mState(state) + { + } + virtual ~RecordBase() = default; virtual std::unique_ptr clone() const = 0; @@ -69,21 +74,18 @@ namespace CSMWorld template Record::Record() - : mBase() + : RecordBase(State_BaseOnly) + , mBase() , mModified() { } template Record::Record(State state, const ESXRecordT* base, const ESXRecordT* modified) + : RecordBase(state) + , mBase(base == nullptr ? ESXRecordT{} : *base) + , mModified(modified == nullptr ? ESXRecordT{} : *modified) { - if (base) - mBase = *base; - - if (modified) - mModified = *modified; - - this->mState = state; } template diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index 328d205106..e28ef64fe7 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -8,8 +8,6 @@ CSMWorld::CellRef::CellRef() : mNew(true) , mIdNum(0) { - mRefNum.mIndex = 0; - mRefNum.mContentFile = 0; } std::pair CSMWorld::CellRef::getCellIndex() const diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index 1afa9027a9..124e697de8 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -47,7 +48,7 @@ namespace CSMWorld } void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Messages& messages) + std::map& cache, CSMDoc::Messages& messages) { Record cell = mCells.getRecord(cellIndex); @@ -64,6 +65,8 @@ void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool b if (!ESM::Cell::getNextRef(reader, ref, isDeleted, mref, isMoved)) break; + if (!base && reader.getIndex() == ref.mRefNum.mContentFile) + ref.mRefNum.mContentFile = -1; // Keep mOriginalCell empty when in modified (as an indicator that the // original cell will always be equal the current cell). ref.mOriginalCell = base ? cell2.mId : ESM::RefId(); @@ -102,16 +105,7 @@ void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool b else ref.mCell = cell2.mId; - if (ref.mRefNum.mContentFile != -1 && !base) - { - ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; - ref.mRefNum.mIndex &= 0x00ffffff; - } - - unsigned int refNum = (ref.mRefNum.mIndex & 0x00ffffff) - | (ref.mRefNum.hasContentFile() ? ref.mRefNum.mContentFile : 0xff) << 24; - - std::map::iterator iter = cache.find(refNum); + auto iter = cache.find(ref.mRefNum); if (isMoved) { @@ -181,7 +175,7 @@ void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool b ref.mIdNum = mNextId; // FIXME: fragile ref.mId = ESM::RefId::stringRefId(getNewId()); - cache.emplace(refNum, ref.mIdNum); + cache.emplace(ref.mRefNum, ref.mIdNum); auto record = std::make_unique>(); record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; @@ -297,6 +291,7 @@ void CSMWorld::RefCollection::cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { auto copy = std::make_unique>(); + int index = getAppendIndex(ESM::RefId(), type); copy->mModified = getRecord(origin).get(); copy->mState = RecordBase::State_ModifiedOnly; @@ -304,6 +299,15 @@ void CSMWorld::RefCollection::cloneRecord( copy->get().mId = destination; copy->get().mIdNum = extractIdNum(destination.getRefIdString()); + if (copy->get().mRefNum.hasContentFile()) + { + mRefIndex.insert(std::make_pair(static_cast*>(copy.get())->get().mIdNum, index)); + copy->get().mRefNum.mContentFile = -1; + copy->get().mRefNum.mIndex = index; + } + else + copy->get().mRefNum.mIndex = copy->get().mIdNum; + insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() } diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index a5e5fd3b6f..d3d200e6c2 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -55,7 +55,7 @@ namespace CSMWorld { } - void load(ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, + void load(ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, CSMDoc::Messages& messages); ///< Load a sequence of references. diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index 149b5d19ca..a6ea56a56d 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -33,7 +34,7 @@ QVariant CSMWorld::PotionRefIdAdapter::getData(const RefIdColumn* column, const data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Potion))); if (column == mAutoCalc) - return record.get().mData.mAutoCalc != 0; + return record.get().mData.mFlags & ESM::Potion::Autocalc; // to show nested tables in dialogue subview, see IdTree::hasChildren() if (column == mColumns.mEffects) @@ -51,7 +52,7 @@ void CSMWorld::PotionRefIdAdapter::setData( ESM::Potion potion = record.get(); if (column == mAutoCalc) - potion.mData.mAutoCalc = value.toInt(); + potion.mData.mFlags = value.toBool(); else { InventoryRefIdAdapter::setData(column, data, index, value); @@ -194,6 +195,26 @@ void CSMWorld::IngredEffectRefIdAdapter::setNestedData( { case 0: ingredient.mData.mEffectID[subRowIndex] = value.toInt(); + switch (ingredient.mData.mEffectID[subRowIndex]) + { + case ESM::MagicEffect::DrainSkill: + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::RestoreSkill: + case ESM::MagicEffect::FortifySkill: + case ESM::MagicEffect::AbsorbSkill: + ingredient.mData.mAttributes[subRowIndex] = -1; + break; + case ESM::MagicEffect::DrainAttribute: + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::RestoreAttribute: + case ESM::MagicEffect::FortifyAttribute: + case ESM::MagicEffect::AbsorbAttribute: + ingredient.mData.mSkills[subRowIndex] = -1; + break; + default: + ingredient.mData.mSkills[subRowIndex] = -1; + ingredient.mData.mAttributes[subRowIndex] = -1; + } break; case 1: ingredient.mData.mSkills[subRowIndex] = value.toInt(); @@ -516,10 +537,18 @@ QVariant CSMWorld::CreatureRefIdAdapter::getData(const RefIdColumn* column, cons if (column == mColumns.mBloodType) return record.get().mBloodType; - std::map::const_iterator iter = mColumns.mFlags.find(column); + { + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter != mColumns.mFlags.end()) - return (record.get().mFlags & iter->second) != 0; + if (iter != mColumns.mFlags.end()) + return (record.get().mFlags & iter->second) != 0; + } + + { + std::map::const_iterator iter = mColumns.mServices.find(column); + if (iter != mColumns.mServices.end() && iter->second == ESM::NPC::Training) + return QVariant(); + } return ActorRefIdAdapter::getData(column, data, index); } @@ -930,30 +959,9 @@ QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData( if (subColIndex == 0) return subRowIndex; - else if (subColIndex == 1) - switch (subRowIndex) - { - case 0: - return static_cast(npcStruct.mStrength); - case 1: - return static_cast(npcStruct.mIntelligence); - case 2: - return static_cast(npcStruct.mWillpower); - case 3: - return static_cast(npcStruct.mAgility); - case 4: - return static_cast(npcStruct.mSpeed); - case 5: - return static_cast(npcStruct.mEndurance); - case 6: - return static_cast(npcStruct.mPersonality); - case 7: - return static_cast(npcStruct.mLuck); - default: - return QVariant(); // throw an exception here? - } - else - return QVariant(); // throw an exception here? + else if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + return static_cast(npcStruct.mAttributes[subRowIndex]); + return QVariant(); // throw an exception here? } void CSMWorld::NpcAttributesRefIdAdapter::setNestedData( @@ -964,36 +972,8 @@ void CSMWorld::NpcAttributesRefIdAdapter::setNestedData( ESM::NPC npc = record.get(); ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt; - if (subColIndex == 1) - switch (subRowIndex) - { - case 0: - npcStruct.mStrength = static_cast(value.toInt()); - break; - case 1: - npcStruct.mIntelligence = static_cast(value.toInt()); - break; - case 2: - npcStruct.mWillpower = static_cast(value.toInt()); - break; - case 3: - npcStruct.mAgility = static_cast(value.toInt()); - break; - case 4: - npcStruct.mSpeed = static_cast(value.toInt()); - break; - case 5: - npcStruct.mEndurance = static_cast(value.toInt()); - break; - case 6: - npcStruct.mPersonality = static_cast(value.toInt()); - break; - case 7: - npcStruct.mLuck = static_cast(value.toInt()); - break; - default: - return; // throw an exception here? - } + if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + npcStruct.mAttributes[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? @@ -1138,11 +1118,11 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData( case 0: return static_cast(record.get().mNpdt.mLevel); case 1: - return QVariant(QVariant::UserType); + return CSMWorld::DisableTag::getVariant(); case 2: - return QVariant(QVariant::UserType); + return CSMWorld::DisableTag::getVariant(); case 3: - return QVariant(QVariant::UserType); + return CSMWorld::DisableTag::getVariant(); case 4: return static_cast(record.get().mNpdt.mDisposition); case 5: @@ -1307,30 +1287,9 @@ QVariant CSMWorld::CreatureAttributesRefIdAdapter::getNestedData( if (subColIndex == 0) return subRowIndex; - else if (subColIndex == 1) - switch (subRowIndex) - { - case 0: - return creature.mData.mStrength; - case 1: - return creature.mData.mIntelligence; - case 2: - return creature.mData.mWillpower; - case 3: - return creature.mData.mAgility; - case 4: - return creature.mData.mSpeed; - case 5: - return creature.mData.mEndurance; - case 6: - return creature.mData.mPersonality; - case 7: - return creature.mData.mLuck; - default: - return QVariant(); // throw an exception here? - } - else - return QVariant(); // throw an exception here? + else if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + return creature.mData.mAttributes[subRowIndex]; + return QVariant(); // throw an exception here? } void CSMWorld::CreatureAttributesRefIdAdapter::setNestedData( @@ -1338,42 +1297,14 @@ void CSMWorld::CreatureAttributesRefIdAdapter::setNestedData( { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Creature))); - ESM::Creature creature = record.get(); - if (subColIndex == 1) - switch (subRowIndex) - { - case 0: - creature.mData.mStrength = value.toInt(); - break; - case 1: - creature.mData.mIntelligence = value.toInt(); - break; - case 2: - creature.mData.mWillpower = value.toInt(); - break; - case 3: - creature.mData.mAgility = value.toInt(); - break; - case 4: - creature.mData.mSpeed = value.toInt(); - break; - case 5: - creature.mData.mEndurance = value.toInt(); - break; - case 6: - creature.mData.mPersonality = value.toInt(); - break; - case 7: - creature.mData.mLuck = value.toInt(); - break; - default: - return; // throw an exception here? - } - else - return; // throw an exception here? - - record.setModified(creature); + if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + { + ESM::Creature creature = record.get(); + creature.mData.mAttributes[subRowIndex] = value.toInt(); + record.setModified(creature); + } + // throw an exception here? } int CSMWorld::CreatureAttributesRefIdAdapter::getNestedColumnsCount( diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index b8c3974d75..e0d5799726 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -97,7 +97,7 @@ CSMWorld::RefIdCollection::RefIdCollection() inventoryColumns.mIcon = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Weight, ColumnBase::Display_Float); inventoryColumns.mWeight = &mColumns.back(); - mColumns.emplace_back(Columns::ColumnId_CoinValue, ColumnBase::Display_Integer); + mColumns.emplace_back(Columns::ColumnId_GoldValue, ColumnBase::Display_Integer); inventoryColumns.mValue = &mColumns.back(); IngredientColumns ingredientColumns(inventoryColumns); @@ -210,7 +210,6 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiDuration, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderToD, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle1, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle2, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle3, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle4, CSMWorld::ColumnBase::Display_Integer)); @@ -218,6 +217,7 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle6, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle7, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle8, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle9, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( diff --git a/apps/opencs/model/world/regionmap.cpp b/apps/opencs/model/world/regionmap.cpp index 5c22aedf4d..79a0d5474d 100644 --- a/apps/opencs/model/world/regionmap.cpp +++ b/apps/opencs/model/world/regionmap.cpp @@ -1,7 +1,9 @@ #include "regionmap.hpp" +#include #include #include +#include #include #include @@ -21,20 +23,33 @@ #include "data.hpp" #include "universalid.hpp" -CSMWorld::RegionMap::CellDescription::CellDescription() - : mDeleted(false) +namespace CSMWorld { + float getLandHeight(const CSMWorld::Cell& cell, CSMWorld::Data& data) + { + const IdCollection& lands = data.getLand(); + int landIndex = lands.searchId(cell.mId); + if (landIndex == -1) + return 0.0f; + + // If any part of land is above water, returns > 0 - otherwise returns < 0 + const Land& land = lands.getRecord(landIndex).get(); + if (land.getLandData()) + return land.getLandData()->mMaxHeight - cell.mWater; + + return 0.0f; + } } -CSMWorld::RegionMap::CellDescription::CellDescription(const Record& cell) +CSMWorld::RegionMap::CellDescription::CellDescription(const Record& cell, float landHeight) { const Cell& cell2 = cell.get(); if (!cell2.isExterior()) throw std::logic_error("Interior cell in region map"); + mMaxLandHeight = landHeight; mDeleted = cell.isDeleted(); - mRegion = cell2.mRegion; mName = cell2.mName; } @@ -92,7 +107,7 @@ void CSMWorld::RegionMap::buildMap() if (cell2.isExterior()) { - CellDescription description(cell); + CellDescription description(cell, getLandHeight(cell2, mData)); CellCoordinates index = getIndex(cell2); @@ -140,7 +155,7 @@ void CSMWorld::RegionMap::addCells(int start, int end) { CellCoordinates index = getIndex(cell2); - CellDescription description(cell); + CellDescription description(cell, getLandHeight(cell.get(), mData)); addCell(index, description); } @@ -335,10 +350,11 @@ QVariant CSMWorld::RegionMap::data(const QModelIndex& index, int role) const auto iter = mColours.find(cell->second.mRegion); if (iter != mColours.end()) - return QBrush(QColor(iter->second & 0xff, (iter->second >> 8) & 0xff, (iter->second >> 16) & 0xff)); + return QBrush(QColor(iter->second & 0xff, (iter->second >> 8) & 0xff, (iter->second >> 16) & 0xff), + cell->second.mMaxLandHeight > 0 ? Qt::SolidPattern : Qt::CrossPattern); - if (cell->second.mRegion.empty()) - return QBrush(Qt::Dense6Pattern); // no region + if (cell->second.mRegion.empty()) // no region + return QBrush(cell->second.mMaxLandHeight > 0 ? Qt::Dense3Pattern : Qt::Dense6Pattern); return QBrush(Qt::red, Qt::Dense6Pattern); // invalid region } diff --git a/apps/opencs/model/world/regionmap.hpp b/apps/opencs/model/world/regionmap.hpp index 3f62c7b61d..96281ba49c 100644 --- a/apps/opencs/model/world/regionmap.hpp +++ b/apps/opencs/model/world/regionmap.hpp @@ -40,13 +40,12 @@ namespace CSMWorld private: struct CellDescription { + float mMaxLandHeight; bool mDeleted; ESM::RefId mRegion; std::string mName; - CellDescription(); - - CellDescription(const Record& cell); + CellDescription(const Record& cell, float landHeight); }; Data& mData; diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index bfab0193b0..7082575c64 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -11,6 +11,7 @@ #include #include +#include CSMWorld::Resources::Resources( const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, const char* const* extensions) @@ -27,20 +28,20 @@ void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char* const* e size_t baseSize = mBaseDirectory.size(); - for (const auto& filepath : vfs->getRecursiveDirectoryIterator("")) + for (const auto& filepath : vfs->getRecursiveDirectoryIterator()) { - if (filepath.size() < baseSize + 1 || filepath.substr(0, baseSize) != mBaseDirectory - || (filepath[baseSize] != '/' && filepath[baseSize] != '\\')) + const std::string_view view = filepath.view(); + if (view.size() < baseSize + 1 || !view.starts_with(mBaseDirectory) || view[baseSize] != '/') continue; if (extensions) { - std::string::size_type extensionIndex = filepath.find_last_of('.'); + const auto extensionIndex = view.find_last_of('.'); - if (extensionIndex == std::string::npos) + if (extensionIndex == std::string_view::npos) continue; - std::string extension = filepath.substr(extensionIndex + 1); + std::string_view extension = view.substr(extensionIndex + 1); int i = 0; @@ -52,10 +53,9 @@ void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char* const* e continue; } - std::string file = filepath.substr(baseSize + 1); + std::string file(view.substr(baseSize + 1)); mFiles.push_back(file); - std::replace(file.begin(), file.end(), '\\', '/'); - mIndex.insert(std::make_pair(Misc::StringUtils::lowerCase(file), static_cast(mFiles.size()) - 1)); + mIndex.emplace(std::move(file), static_cast(mFiles.size()) - 1); } } diff --git a/apps/opencs/model/world/subcellcollection.hpp b/apps/opencs/model/world/subcellcollection.hpp index f7cb896aca..a025d407c7 100644 --- a/apps/opencs/model/world/subcellcollection.hpp +++ b/apps/opencs/model/world/subcellcollection.hpp @@ -18,14 +18,15 @@ namespace CSMWorld { const IdCollection& mCells; - void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) override; + void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) override; public: SubCellCollection(const IdCollection& cells); }; template - void SubCellCollection::loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) + void SubCellCollection::loadRecord( + ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { record.load(reader, isDeleted, mCells); } diff --git a/apps/opencs/model/world/tablemimedata.cpp b/apps/opencs/model/world/tablemimedata.cpp index a966a53eee..a40698bc27 100644 --- a/apps/opencs/model/world/tablemimedata.cpp +++ b/apps/opencs/model/world/tablemimedata.cpp @@ -58,7 +58,7 @@ std::string CSMWorld::TableMimeData::getIcon() const if (tmpIcon != id.getIcon()) { - return ":/multitype.png"; // icon stolen from gnome TODO: get new icon + return ":multitype"; } tmpIcon = id.getIcon(); diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index dec533b015..0ebccd6253 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -23,157 +23,137 @@ namespace constexpr TypeData sNoArg[] = { { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, "-", ":placeholder" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Globals, "Global Variables", - ":./global-variable.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Gmsts, "Game Settings", ":./gmst.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Skills, "Skills", ":./skill.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Classes, "Classes", ":./class.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Factions, "Factions", ":./faction.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Races, "Races", ":./race.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Sounds, "Sounds", ":./sound.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Scripts, "Scripts", ":./script.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Regions, "Regions", ":./region.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Birthsigns, "Birthsigns", - ":./birthsign.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Spells, "Spells", ":./spell.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Topics, "Topics", - ":./dialogue-topics.png" }, + ":global-variable" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Gmsts, "Game Settings", ":gmst" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Skills, "Skills", ":skill" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Classes, "Classes", ":class" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Factions, "Factions", ":faction" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Races, "Races", ":race" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Sounds, "Sounds", ":sound" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Scripts, "Scripts", ":script" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Regions, "Regions", ":region" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Birthsigns, "Birthsigns", ":birthsign" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Spells, "Spells", ":spell" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Topics, "Topics", ":dialogue-topics" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Journals, "Journals", - ":./journal-topics.png" }, + ":journal-topics" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_TopicInfos, "Topic Infos", - ":./dialogue-topic-infos.png" }, + ":dialogue-info" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_JournalInfos, "Journal Infos", - ":./journal-topic-infos.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Cells, "Cells", ":./cell.png" }, + ":journal-topic-infos" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Cells, "Cells", ":cell" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Enchantments, "Enchantments", - ":./enchantment.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_BodyParts, "Body Parts", - ":./body-part.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Referenceables, "Objects", - ":./object.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, "Instances", - ":./instance.png" }, - { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, "Region Map", - ":./region-map.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", ":./filter.png" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", - ":./resources-mesh" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", ":./resources-icon" }, + ":enchantment" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_BodyParts, "Body Parts", ":body-part" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Referenceables, "Objects", ":object" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, "Instances", ":instance" }, + { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, "Region Map", ":region-map" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", ":filter" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", ":resources-mesh" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", ":resources-icon" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Music Files", - ":./resources-music" }, + ":resources-music" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_SoundsRes, "Sound Files", ":resources-sound" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Textures, "Textures", - ":./resources-texture" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", - ":./resources-video" }, + ":resources-texture" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", ":resources-video" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", - ":./debug-profile.png" }, - { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", ":./run-log.png" }, + ":debug-profile" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SelectionGroup, "Selection Groups", "" }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", ":run-log" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", - ":./sound-generator.png" }, + ":sound-generator" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", - ":./magic-effect.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Lands, "Lands", - ":./land-heightmap.png" }, + ":magic-effect" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Lands, "Lands", ":land-heightmap" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_LandTextures, "Land Textures", - ":./land-texture.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", - ":./pathgrid.png" }, + ":land-texture" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", ":pathgrid" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_StartScripts, "Start Scripts", - ":./start-script.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MetaDatas, "Metadata", - ":./metadata.png" }, + ":start-script" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MetaDatas, "Metadata", ":metadata" }, }; constexpr TypeData sIdArg[] = { { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Global, "Global Variable", - ":./global-variable.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Gmst, "Game Setting", ":./gmst.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Skill, "Skill", ":./skill.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Class, "Class", ":./class.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Faction, "Faction", ":./faction.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Race, "Race", ":./race.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Sound, "Sound", ":./sound.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Script, "Script", ":./script.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Region, "Region", ":./region.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Birthsign, "Birthsign", ":./birthsign.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Spell, "Spell", ":./spell.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Topic, "Topic", ":./dialogue-topics.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Journal, "Journal", - ":./journal-topics.png" }, + ":global-variable" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Gmst, "Game Setting", ":gmst" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Skill, "Skill", ":skill" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Class, "Class", ":class" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Faction, "Faction", ":faction" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Race, "Race", ":race" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Sound, "Sound", ":sound" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Script, "Script", ":script" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Region, "Region", ":region" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Birthsign, "Birthsign", ":birthsign" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Spell, "Spell", ":spell" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Topic, "Topic", ":dialogue-topics" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Journal, "Journal", ":journal-topics" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_TopicInfo, "TopicInfo", - ":./dialogue-topic-infos.png" }, + ":dialogue-info" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_JournalInfo, "JournalInfo", - ":./journal-topic-infos.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell, "Cell", ":./cell.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell_Missing, "Cell", ":./cell.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Referenceable, "Object", ":./object.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Activator, "Activator", - ":./activator.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Potion, "Potion", ":./potion.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Apparatus, "Apparatus", - ":./apparatus.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Armor, "Armor", ":./armor.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Book, "Book", ":./book.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Clothing, "Clothing", ":./clothing.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Container, "Container", - ":./container.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Creature, "Creature", ":./creature.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Door, "Door", ":./door.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Ingredient, "Ingredient", - ":./ingredient.png" }, + ":journal-topic-infos" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell, "Cell", ":cell" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell_Missing, "Cell", ":cell" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Referenceable, "Object", ":object" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Activator, "Activator", ":activator" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Potion, "Potion", ":potion" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Apparatus, "Apparatus", ":apparatus" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Armor, "Armor", ":armor" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Book, "Book", ":book" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Clothing, "Clothing", ":clothing" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Container, "Container", ":container" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Creature, "Creature", ":creature" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Door, "Door", ":door" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Ingredient, "Ingredient", ":ingredient" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_CreatureLevelledList, - "Creature Levelled List", ":./levelled-creature.png" }, + "Creature Levelled List", ":levelled-creature" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_ItemLevelledList, "Item Levelled List", - ":./levelled-item.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Light, "Light", ":./light.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Lockpick, "Lockpick", ":./lockpick.png" }, + ":levelled-item" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Light, "Light", ":light" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Lockpick, "Lockpick", ":lockpick" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Miscellaneous, "Miscellaneous", - ":./miscellaneous.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Npc, "NPC", ":./npc.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Probe, "Probe", ":./probe.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Repair, "Repair", ":./repair.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":./static.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":./weapon.png" }, - { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Instance", - ":./instance.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Filter, "Filter", ":./filter.png" }, - { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Scene, "Scene", ":./scene.png" }, - { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Preview, "Preview", - ":./record-preview.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Enchantment, "Enchantment", - ":./enchantment.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_BodyPart, "Body Part", ":./body-part.png" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Mesh, "Mesh", ":./resources-mesh" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Icon, "Icon", ":./resources-icon" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Music, "Music", ":./resources-music" }, + ":miscellaneous" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Npc, "NPC", ":npc" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Probe, "Probe", ":probe" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Repair, "Repair", ":repair" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":static" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":weapon" }, + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Instance", ":instance" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Filter, "Filter", ":filter" }, + { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Scene, "Scene", ":scene" }, + { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Preview, "Preview", ":edit-preview" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Enchantment, "Enchantment", ":enchantment" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_BodyPart, "Body Part", ":body-part" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Mesh, "Mesh", ":resources-mesh" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Icon, "Icon", ":resources-icon" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Music, "Music", ":resources-music" }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_SoundRes, "Sound File", - ":./resources-sound" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", - ":./resources-texture" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", ":./resources-video" }, + ":resources-sound" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", ":resources-texture" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", ":resources-video" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", - ":./debug-profile.png" }, + ":debug-profile" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", - ":./sound-generator.png" }, + ":sound-generator" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", - ":./magic-effect.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Land, "Land", ":./land-heightmap.png" }, + ":magic-effect" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Land, "Land", ":land-heightmap" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_LandTexture, "Land Texture", - ":./land-texture.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", ":./pathgrid.png" }, + ":land-texture" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", ":pathgrid" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_StartScript, "Start Script", - ":./start-script.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MetaData, "Metadata", ":./metadata.png" }, + ":start-script" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MetaData, "Metadata", ":metadata" }, }; constexpr TypeData sIndexArg[] = { { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_VerificationResults, - "Verification Results", ":./menu-verify.png" }, + "Verification Results", ":menu-verify" }, { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_LoadErrorLog, "Load Error Log", - ":./error-log.png" }, - { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_Search, "Global Search", - ":./menu-search.png" }, + ":error-log" }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_Search, "Global Search", ":menu-search" }, }; struct WriteToStream @@ -187,6 +167,8 @@ namespace { mStream << ": " << value; } + + void operator()(const ESM::RefId& value) const { mStream << ": " << value.toString(); } }; struct GetTypeData @@ -218,6 +200,23 @@ namespace return std::to_string(value); } + + CSMWorld::UniversalId::Class getClassByType(CSMWorld::UniversalId::Type type) + { + if (const auto it + = std::find_if(std::begin(sIdArg), std::end(sIdArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sIdArg)) + return it->mClass; + if (const auto it = std::find_if( + std::begin(sIndexArg), std::end(sIndexArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sIndexArg)) + return it->mClass; + if (const auto it + = std::find_if(std::begin(sNoArg), std::end(sNoArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sNoArg)) + return it->mClass; + throw std::logic_error("invalid UniversalId type: " + std::to_string(type)); + } } CSMWorld::UniversalId::UniversalId(const std::string& universalId) @@ -326,6 +325,13 @@ CSMWorld::UniversalId::UniversalId(Type type, ESM::RefId id) throw std::logic_error("invalid RefId argument UniversalId type: " + std::to_string(type)); } +CSMWorld::UniversalId::UniversalId(Type type, const UniversalId& id) + : mClass(getClassByType(type)) + , mType(type) + , mValue(id.mValue) +{ +} + CSMWorld::UniversalId::UniversalId(Type type, int index) : mType(type) , mValue(index) @@ -360,6 +366,10 @@ const std::string& CSMWorld::UniversalId::getId() const if (const std::string* result = std::get_if(&mValue)) return *result; + if (const ESM::RefId* refId = std::get_if(&mValue)) + if (const ESM::StringRefId* result = refId->getIf()) + return result->getValue(); + throw std::logic_error("invalid access to ID of " + ::toString(getArgumentType()) + " UniversalId"); } diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 2d3385bcb4..34ef480fa5 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -133,6 +133,7 @@ namespace CSMWorld Type_LandTexture, Type_Pathgrids, Type_Pathgrid, + Type_SelectionGroup, Type_StartScripts, Type_StartScript, Type_Search, @@ -158,6 +159,8 @@ namespace CSMWorld UniversalId(Type type, int index); ///< Using a type for a non-index-argument UniversalId will throw an exception. + UniversalId(Type type, const UniversalId& id); + Class getClass() const; ArgumentType getArgumentType() const; diff --git a/files/ui/filedialog.ui b/apps/opencs/ui/filedialog.ui similarity index 100% rename from files/ui/filedialog.ui rename to apps/opencs/ui/filedialog.ui diff --git a/apps/opencs/view/doc/adjusterwidget.cpp b/apps/opencs/view/doc/adjusterwidget.cpp index d4cfdc6d3e..a282ebcaff 100644 --- a/apps/opencs/view/doc/adjusterwidget.cpp +++ b/apps/opencs/view/doc/adjusterwidget.cpp @@ -91,7 +91,7 @@ void CSVDoc::AdjusterWidget::setName(const QString& name, bool addon) { // path already points to the local data directory message = "Will be saved as: " + Files::pathToQString(path); - mResultPath = path; + mResultPath = std::move(path); } // in all other cases, ensure the path points to data-local and do an existing file check else diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 1b502c9d6c..455fda20c2 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -13,27 +13,32 @@ #include "adjusterwidget.hpp" #include "filewidget.hpp" +#include "ui_filedialog.h" + CSVDoc::FileDialog::FileDialog(QWidget* parent) : QDialog(parent) , mSelector(nullptr) + , ui(std::make_unique()) , mAction(ContentAction_Undefined) , mFileWidget(nullptr) , mAdjusterWidget(nullptr) , mDialogBuilt(false) { - ui.setupUi(this); + ui->setupUi(this); resize(400, 400); setObjectName("FileDialog"); - mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/false); + mSelector = new ContentSelectorView::ContentSelector(ui->contentSelectorWidget, /*showOMWScripts=*/false); mAdjusterWidget = new AdjusterWidget(this); } +CSVDoc::FileDialog::~FileDialog() = default; + void CSVDoc::FileDialog::addFiles(const std::vector& dataDirs) { - for (auto iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) + for (const auto& dir : dataDirs) { - QString path = Files::pathToQString(*iter); + QString path = Files::pathToQString(dir); mSelector->addFiles(path); } mSelector->sortFiles(); @@ -68,7 +73,7 @@ void CSVDoc::FileDialog::showDialog(ContentAction action) { mAction = action; - ui.projectGroupBoxLayout->insertWidget(0, mAdjusterWidget); + ui->projectGroupBoxLayout->insertWidget(0, mAdjusterWidget); switch (mAction) { @@ -92,7 +97,7 @@ void CSVDoc::FileDialog::showDialog(ContentAction action) connect(mSelector, &ContentSelectorView::ContentSelector::signalCurrentGamefileIndexChanged, this, qOverload(&FileDialog::slotUpdateAcceptButton)); - connect(ui.projectButtonBox, &QDialogButtonBox::rejected, this, &FileDialog::slotRejected); + connect(ui->projectButtonBox, &QDialogButtonBox::rejected, this, &FileDialog::slotRejected); mDialogBuilt = true; } @@ -105,7 +110,7 @@ void CSVDoc::FileDialog::buildNewFileView() { setWindowTitle(tr("Create a new addon")); - QPushButton* createButton = ui.projectButtonBox->button(QDialogButtonBox::Ok); + QPushButton* createButton = ui->projectButtonBox->button(QDialogButtonBox::Ok); createButton->setText("Create"); createButton->setEnabled(false); @@ -122,27 +127,27 @@ void CSVDoc::FileDialog::buildNewFileView() qOverload(&FileDialog::slotUpdateAcceptButton)); } - ui.projectGroupBoxLayout->insertWidget(0, mFileWidget); + ui->projectGroupBoxLayout->insertWidget(0, mFileWidget); - connect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); + connect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); } void CSVDoc::FileDialog::buildOpenFileView() { setWindowTitle(tr("Open")); - ui.projectGroupBox->setTitle(QString("")); - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setText("Open"); + ui->projectGroupBox->setTitle(QString("")); + ui->projectButtonBox->button(QDialogButtonBox::Ok)->setText("Open"); if (mSelector->isGamefileSelected()) - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + ui->projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(true); else - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); if (!mDialogBuilt) { connect(mSelector, &ContentSelectorView::ContentSelector::signalAddonDataChanged, this, &FileDialog::slotAddonDataChanged); } - connect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); + connect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); } void CSVDoc::FileDialog::slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright) @@ -176,7 +181,7 @@ void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString& name, bool) else mAdjusterWidget->setName("", true); - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(success); + ui->projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(success); } QString CSVDoc::FileDialog::filename() const @@ -190,8 +195,8 @@ QString CSVDoc::FileDialog::filename() const void CSVDoc::FileDialog::slotRejected() { emit rejected(); - disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); - disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); + disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); + disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); if (mFileWidget) { delete mFileWidget; @@ -208,7 +213,7 @@ void CSVDoc::FileDialog::slotNewFile() delete mFileWidget; mFileWidget = nullptr; } - disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); + disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); close(); } @@ -219,6 +224,6 @@ void CSVDoc::FileDialog::slotOpenFile() mAdjusterWidget->setName(file->filePath(), !file->isGameFile()); emit signalOpenFiles(mAdjusterWidget->getPath()); - disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); + disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); close(); } diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp index d660f22f75..a8a73439c7 100644 --- a/apps/opencs/view/doc/filedialog.hpp +++ b/apps/opencs/view/doc/filedialog.hpp @@ -14,8 +14,6 @@ Q_DECLARE_METATYPE(std::filesystem::path) #endif -#include "ui_filedialog.h" - #include #include @@ -26,6 +24,11 @@ namespace ContentSelectorView class ContentSelector; } +namespace Ui +{ + class FileDialog; +} + namespace CSVDoc { class FileWidget; @@ -36,7 +39,7 @@ namespace CSVDoc private: ContentSelectorView::ContentSelector* mSelector; - Ui::FileDialog ui; + std::unique_ptr ui; ContentAction mAction; FileWidget* mFileWidget; AdjusterWidget* mAdjusterWidget; @@ -44,6 +47,9 @@ namespace CSVDoc public: explicit FileDialog(QWidget* parent = nullptr); + + ~FileDialog(); + void showDialog(ContentAction action); void addFiles(const std::vector& dataDirs); diff --git a/apps/opencs/view/doc/operation.cpp b/apps/opencs/view/doc/operation.cpp index 4825f87ff3..571413af1b 100644 --- a/apps/opencs/view/doc/operation.cpp +++ b/apps/opencs/view/doc/operation.cpp @@ -13,21 +13,21 @@ void CSVDoc::Operation::updateLabel(int threads) { if (threads == -1 || ((threads == 0) != mStalling)) { - std::string name("unknown operation"); + std::string name("Unknown operation"); switch (mType) { case CSMDoc::State_Saving: - name = "saving"; + name = "Saving"; break; case CSMDoc::State_Verifying: - name = "verifying"; + name = "Verifying"; break; case CSMDoc::State_Searching: - name = "searching"; + name = "Searching"; break; case CSMDoc::State_Merging: - name = "merging"; + name = "Merging"; break; } @@ -70,6 +70,7 @@ void CSVDoc::Operation::initWidgets() mProgressBar = new QProgressBar(); mAbortButton = new QPushButton("Abort"); mLayout = new QHBoxLayout(); + mLayout->setContentsMargins(8, 4, 8, 4); mLayout->addWidget(mProgressBar); mLayout->addWidget(mAbortButton); @@ -94,17 +95,18 @@ void CSVDoc::Operation::setBarColor(int type) QString style = "QProgressBar {" "text-align: center;" + "border: 1px solid #4e4e4e;" "}" "QProgressBar::chunk {" "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:.50 %2 stop: .51 %3 stop:1 %4);" "text-align: center;" - "margin: 2px 1px 1p 2px;" + "margin: 2px;" "}"; - QString topColor = "#F2F6F8"; - QString bottomColor = "#E0EFF9"; - QString midTopColor = "#D8E1E7"; - QString midBottomColor = "#B5C6D0"; // default gray gloss + QString topColor = "#9e9e9e"; + QString bottomColor = "#919191"; + QString midTopColor = "#848484"; + QString midBottomColor = "#717171"; // default gray // colors inspired by samples from: // http://www.colorzilla.com/gradient-editor/ @@ -113,42 +115,35 @@ void CSVDoc::Operation::setBarColor(int type) { case CSMDoc::State_Saving: - topColor = "#FECCB1"; - midTopColor = "#F17432"; - midBottomColor = "#EA5507"; - bottomColor = "#FB955E"; // red gloss #2 + topColor = "#f27d6e"; + midTopColor = "#ee6954"; + midBottomColor = "#f05536"; + bottomColor = "#de511e"; // red break; case CSMDoc::State_Searching: - topColor = "#EBF1F6"; - midTopColor = "#ABD3EE"; - midBottomColor = "#89C3EB"; - bottomColor = "#D5EBFB"; // blue gloss #4 + topColor = "#6db3f2"; + midTopColor = "#54a3ee"; + midBottomColor = "#3690f0"; + bottomColor = "#1e69de"; // blue break; case CSMDoc::State_Verifying: - topColor = "#BFD255"; - midTopColor = "#8EB92A"; - midBottomColor = "#72AA00"; - bottomColor = "#9ECB2D"; // green gloss + topColor = "#bfd255"; + midTopColor = "#8eb92a"; + midBottomColor = "#72aa00"; + bottomColor = "#9ecb2d"; // green break; case CSMDoc::State_Merging: - topColor = "#F3E2C7"; - midTopColor = "#C19E67"; - midBottomColor = "#B68D4C"; - bottomColor = "#E9D4B3"; // l Brown 3D + topColor = "#d89188"; + midTopColor = "#d07f72"; + midBottomColor = "#cc6d5a"; + bottomColor = "#b86344"; // brown break; - - default: - - topColor = "#F2F6F8"; - bottomColor = "#E0EFF9"; - midTopColor = "#D8E1E7"; - midBottomColor = "#B5C6D0"; // gray gloss for undefined ops } mProgressBar->setStyleSheet(style.arg(topColor).arg(midTopColor).arg(midBottomColor).arg(bottomColor)); diff --git a/apps/opencs/view/doc/operations.cpp b/apps/opencs/view/doc/operations.cpp index 4fe53d6297..97f47d621b 100644 --- a/apps/opencs/view/doc/operations.cpp +++ b/apps/opencs/view/doc/operations.cpp @@ -8,19 +8,24 @@ #include "operation.hpp" +namespace +{ + constexpr int operationLineHeight = 36; +} + CSVDoc::Operations::Operations() { - /// \todo make widget height fixed (exactly the height required to display all operations) - setFeatures(QDockWidget::NoDockWidgetFeatures); QWidget* widgetContainer = new QWidget(this); mLayout = new QVBoxLayout; + mLayout->setContentsMargins(0, 0, 0, 0); + widgetContainer->setContentsMargins(0, 0, 0, 0); widgetContainer->setLayout(mLayout); setWidget(widgetContainer); setVisible(false); - setFixedHeight(widgetContainer->height()); + setFixedHeight(operationLineHeight); setTitleBarWidget(new QWidget(this)); } @@ -33,9 +38,6 @@ void CSVDoc::Operations::setProgress(int current, int max, int type, int threads return; } - int oldCount = static_cast(mOperations.size()); - int newCount = oldCount + 1; - Operation* operation = new Operation(type, this); connect(operation, qOverload(&Operation::abortOperation), this, &Operations::abortOperation); @@ -43,8 +45,8 @@ void CSVDoc::Operations::setProgress(int current, int max, int type, int threads mOperations.push_back(operation); operation->setProgress(current, max, threads); - if (oldCount > 0) - setFixedHeight(height() / oldCount * newCount); + int newCount = static_cast(mOperations.size()); + setFixedHeight(operationLineHeight * newCount); setVisible(true); } @@ -54,16 +56,14 @@ void CSVDoc::Operations::quitOperation(int type) for (std::vector::iterator iter(mOperations.begin()); iter != mOperations.end(); ++iter) if ((*iter)->getType() == type) { - int oldCount = static_cast(mOperations.size()); - int newCount = oldCount - 1; - mLayout->removeItem((*iter)->getLayout()); (*iter)->deleteLater(); mOperations.erase(iter); - if (oldCount > 1) - setFixedHeight(height() / oldCount * newCount); + int newCount = static_cast(mOperations.size()); + if (newCount > 0) + setFixedHeight(operationLineHeight * newCount); else setVisible(false); diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 27463b0456..c6e109355f 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -1,5 +1,7 @@ #include "startup.hpp" +#include + #include #include #include @@ -10,13 +12,13 @@ #include #include -QPushButton* CSVDoc::StartupDialogue::addButton(const QString& label, const QIcon& icon) +QPushButton* CSVDoc::StartupDialogue::addButton(const QString& label, const QString& icon) { int column = mColumn--; QPushButton* button = new QPushButton(this); - button->setIcon(QIcon(icon)); + button->setIcon(Misc::ScalableIcon::load(icon)); button->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred)); @@ -39,13 +41,13 @@ QWidget* CSVDoc::StartupDialogue::createButtons() mLayout = new QGridLayout(widget); /// \todo add icons - QPushButton* loadDocument = addButton("Edit A Content File", QIcon(":startup/edit-content")); + QPushButton* loadDocument = addButton("Edit A Content File", ":startup/edit-content"); connect(loadDocument, &QPushButton::clicked, this, &StartupDialogue::loadDocument); - QPushButton* createAddon = addButton("Create A New Addon", QIcon(":startup/create-addon")); + QPushButton* createAddon = addButton("Create A New Addon", ":startup/create-addon"); connect(createAddon, &QPushButton::clicked, this, &StartupDialogue::createAddon); - QPushButton* createGame = addButton("Create A New Game", QIcon(":startup/create-game")); + QPushButton* createGame = addButton("Create A New Game", ":startup/create-game"); connect(createGame, &QPushButton::clicked, this, &StartupDialogue::createGame); for (int i = 0; i < 3; ++i) @@ -78,7 +80,7 @@ QWidget* CSVDoc::StartupDialogue::createTools() QPushButton* config = new QPushButton(widget); config->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); - config->setIcon(QIcon(":startup/configure")); + config->setIcon(Misc::ScalableIcon::load(":startup/configure")); config->setToolTip("Open user settings"); layout->addWidget(config); diff --git a/apps/opencs/view/doc/startup.hpp b/apps/opencs/view/doc/startup.hpp index 061b91b2d1..f2cccfcd38 100644 --- a/apps/opencs/view/doc/startup.hpp +++ b/apps/opencs/view/doc/startup.hpp @@ -20,7 +20,7 @@ namespace CSVDoc int mColumn; QGridLayout* mLayout; - QPushButton* addButton(const QString& label, const QIcon& icon); + QPushButton* addButton(const QString& label, const QString& icon); QWidget* createButtons(); diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 08db4efa5e..7ee8092df6 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -36,6 +36,8 @@ #include #include +#include +#include #include #include @@ -71,30 +73,30 @@ void CSVDoc::View::setupFileMenu() { QMenu* file = menuBar()->addMenu(tr("File")); - QAction* newGame = createMenuEntry("New Game", ":./menu-new-game.png", file, "document-file-newgame"); + QAction* newGame = createMenuEntry("New Game", ":menu-new-game", file, "document-file-newgame"); connect(newGame, &QAction::triggered, this, &View::newGameRequest); - QAction* newAddon = createMenuEntry("New Addon", ":./menu-new-addon.png", file, "document-file-newaddon"); + QAction* newAddon = createMenuEntry("New Addon", ":menu-new-addon", file, "document-file-newaddon"); connect(newAddon, &QAction::triggered, this, &View::newAddonRequest); - QAction* open = createMenuEntry("Open", ":./menu-open.png", file, "document-file-open"); + QAction* open = createMenuEntry("Open", ":menu-open", file, "document-file-open"); connect(open, &QAction::triggered, this, &View::loadDocumentRequest); - QAction* save = createMenuEntry("Save", ":./menu-save.png", file, "document-file-save"); + QAction* save = createMenuEntry("Save", ":menu-save", file, "document-file-save"); connect(save, &QAction::triggered, this, &View::save); mSave = save; file->addSeparator(); - QAction* verify = createMenuEntry("Verify", ":./menu-verify.png", file, "document-file-verify"); + QAction* verify = createMenuEntry("Verify", ":menu-verify", file, "document-file-verify"); connect(verify, &QAction::triggered, this, &View::verify); mVerify = verify; - QAction* merge = createMenuEntry("Merge", ":./menu-merge.png", file, "document-file-merge"); + QAction* merge = createMenuEntry("Merge", ":menu-merge", file, "document-file-merge"); connect(merge, &QAction::triggered, this, &View::merge); mMerge = merge; - QAction* loadErrors = createMenuEntry("Error Log", ":./error-log.png", file, "document-file-errorlog"); + QAction* loadErrors = createMenuEntry("Error Log", ":error-log", file, "document-file-errorlog"); connect(loadErrors, &QAction::triggered, this, &View::loadErrorLog); QAction* meta = createMenuEntry(CSMWorld::UniversalId::Type_MetaDatas, file, "document-file-metadata"); @@ -102,10 +104,10 @@ void CSVDoc::View::setupFileMenu() file->addSeparator(); - QAction* close = createMenuEntry("Close", ":./menu-close.png", file, "document-file-close"); + QAction* close = createMenuEntry("Close", ":menu-close", file, "document-file-close"); connect(close, &QAction::triggered, this, &View::close); - QAction* exit = createMenuEntry("Exit", ":./menu-exit.png", file, "document-file-exit"); + QAction* exit = createMenuEntry("Exit", ":menu-exit", file, "document-file-exit"); connect(exit, &QAction::triggered, this, &View::exit); connect(this, &View::exitApplicationRequest, &mViewManager, &ViewManager::exitApplication); @@ -140,17 +142,16 @@ void CSVDoc::View::setupEditMenu() mUndo = mDocument->getUndoStack().createUndoAction(this, tr("Undo")); setupShortcut("document-edit-undo", mUndo); connect(mUndo, &QAction::changed, this, &View::undoActionChanged); - mUndo->setIcon(QIcon(QString::fromStdString(":./menu-undo.png"))); + mUndo->setIcon(Misc::ScalableIcon::load(":menu-undo")); edit->addAction(mUndo); mRedo = mDocument->getUndoStack().createRedoAction(this, tr("Redo")); connect(mRedo, &QAction::changed, this, &View::redoActionChanged); setupShortcut("document-edit-redo", mRedo); - mRedo->setIcon(QIcon(QString::fromStdString(":./menu-redo.png"))); + mRedo->setIcon(Misc::ScalableIcon::load(":menu-redo")); edit->addAction(mRedo); - QAction* userSettings - = createMenuEntry("Preferences", ":./menu-preferences.png", edit, "document-edit-preferences"); + QAction* userSettings = createMenuEntry("Preferences", ":menu-preferences", edit, "document-edit-preferences"); connect(userSettings, &QAction::triggered, this, &View::editSettingsRequest); QAction* search = createMenuEntry(CSMWorld::UniversalId::Type_Search, edit, "document-edit-search"); @@ -161,10 +162,10 @@ void CSVDoc::View::setupViewMenu() { QMenu* view = menuBar()->addMenu(tr("View")); - QAction* newWindow = createMenuEntry("New View", ":./menu-new-window.png", view, "document-view-newview"); + QAction* newWindow = createMenuEntry("New View", ":menu-new-window", view, "document-view-newview"); connect(newWindow, &QAction::triggered, this, &View::newView); - mShowStatusBar = createMenuEntry("Toggle Status Bar", ":./menu-status-bar.png", view, "document-view-statusbar"); + mShowStatusBar = createMenuEntry("Toggle Status Bar", ":menu-status-bar", view, "document-view-statusbar"); connect(mShowStatusBar, &QAction::toggled, this, &View::toggleShowStatusBar); mShowStatusBar->setCheckable(true); mShowStatusBar->setChecked(CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); @@ -289,7 +290,7 @@ void CSVDoc::View::setupAssetsMenu() { QMenu* assets = menuBar()->addMenu(tr("Assets")); - QAction* reload = createMenuEntry("Reload", ":./menu-reload.png", assets, "document-assets-reload"); + QAction* reload = createMenuEntry("Reload", ":menu-reload", assets, "document-assets-reload"); connect(reload, &QAction::triggered, &mDocument->getData(), &CSMWorld::Data::assetsChanged); assets->addSeparator(); @@ -341,9 +342,9 @@ void CSVDoc::View::setupDebugMenu() QAction* runDebug = debug->addMenu(mGlobalDebugProfileMenu); runDebug->setText(tr("Run OpenMW")); setupShortcut("document-debug-run", runDebug); - runDebug->setIcon(QIcon(QString::fromStdString(":./run-openmw.png"))); + runDebug->setIcon(Misc::ScalableIcon::load(":run-openmw")); - QAction* stopDebug = createMenuEntry("Stop OpenMW", ":./stop-openmw.png", debug, "document-debug-shutdown"); + QAction* stopDebug = createMenuEntry("Stop OpenMW", ":stop-openmw", debug, "document-debug-shutdown"); connect(stopDebug, &QAction::triggered, this, &View::stop); mStopDebug = stopDebug; @@ -355,16 +356,16 @@ void CSVDoc::View::setupHelpMenu() { QMenu* help = menuBar()->addMenu(tr("Help")); - QAction* helpInfo = createMenuEntry("Help", ":/info.png", help, "document-help-help"); + QAction* helpInfo = createMenuEntry("Help", ":info", help, "document-help-help"); connect(helpInfo, &QAction::triggered, this, &View::openHelp); - QAction* tutorial = createMenuEntry("Tutorial", ":/info.png", help, "document-help-tutorial"); + QAction* tutorial = createMenuEntry("Tutorial", ":info", help, "document-help-tutorial"); connect(tutorial, &QAction::triggered, this, &View::tutorial); - QAction* about = createMenuEntry("About OpenMW-CS", ":./info.png", help, "document-help-about"); + QAction* about = createMenuEntry("About OpenMW-CS", ":info", help, "document-help-about"); connect(about, &QAction::triggered, this, &View::infoAbout); - QAction* aboutQt = createMenuEntry("About Qt", ":./qt.png", help, "document-help-qt"); + QAction* aboutQt = createMenuEntry("About Qt", ":qt", help, "document-help-qt"); connect(aboutQt, &QAction::triggered, this, &View::infoAboutQt); } @@ -375,7 +376,7 @@ QAction* CSVDoc::View::createMenuEntry(CSMWorld::UniversalId::Type type, QMenu* setupShortcut(shortcutName, entry); const std::string iconName = CSMWorld::UniversalId(type).getIcon(); if (!iconName.empty() && iconName != ":placeholder") - entry->setIcon(QIcon(QString::fromStdString(iconName))); + entry->setIcon(Misc::ScalableIcon::load(QString::fromStdString(iconName))); menu->addAction(entry); @@ -388,7 +389,7 @@ QAction* CSVDoc::View::createMenuEntry( QAction* entry = new QAction(QString::fromStdString(title), this); setupShortcut(shortcutName, entry); if (!iconName.empty() && iconName != ":placeholder") - entry->setIcon(QIcon(QString::fromStdString(iconName))); + entry->setIcon(Misc::ScalableIcon::load(QString::fromStdString(iconName))); menu->addAction(entry); @@ -629,7 +630,7 @@ void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string if (isReferenceable) { view = mSubViewFactory.makeSubView( - CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); + CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id), *mDocument); } else { @@ -774,7 +775,7 @@ void CSVDoc::View::tutorial() void CSVDoc::View::infoAbout() { // Get current OpenMW version - QString versionInfo = (Version::getOpenmwVersionDescription(mDocument->getResourceDir()) + + QString versionInfo = (Version::getOpenmwVersionDescription() + #if defined(__x86_64__) || defined(_M_X64) " (64-bit)") .c_str(); @@ -1111,7 +1112,10 @@ void CSVDoc::View::updateWidth(bool isGrowLimit, int minSubViewWidth) { QRect rect; if (isGrowLimit) - rect = QApplication::screenAt(pos())->geometry(); + { + QScreen* screen = getWidgetScreen(pos()); + rect = screen->geometry(); + } else rect = desktopRect(); @@ -1156,3 +1160,27 @@ void CSVDoc::View::onRequestFocus(const std::string& id) addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Reference, id)); } } + +QScreen* CSVDoc::View::getWidgetScreen(const QPoint& position) +{ + QScreen* screen = QApplication::screenAt(position); + if (screen == nullptr) + { + QPoint clampedPosition = position; + + // If we failed to find the screen, + // clamp negative positions and try again + if (clampedPosition.x() <= 0) + clampedPosition.setX(0); + if (clampedPosition.y() <= 0) + clampedPosition.setY(0); + + screen = QApplication::screenAt(clampedPosition); + } + + if (screen == nullptr) + throw std::runtime_error( + Misc::StringUtils::format("Can not detect the screen for position [%d, %d]", position.x(), position.y())); + + return screen; +} diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 165cb0da8e..bdf051a80b 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -108,6 +108,8 @@ namespace CSVDoc View& operator=(const View&) = delete; ~View() override = default; + static QScreen* getWidgetScreen(const QPoint& position); + const CSMDoc::Document* getDocument() const; CSMDoc::Document* getDocument(); diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index dff4426ba5..812a1bd534 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -410,7 +410,7 @@ bool CSVDoc::ViewManager::removeDocument(CSVDoc::View* view) remainingViews.push_back(*iter); } mDocumentManager.removeDocument(document); - mViews = remainingViews; + mViews = std::move(remainingViews); } return true; } diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 50735bf703..8076e99e63 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "../../model/prefs/shortcut.hpp" #include "../../model/world/columns.hpp" @@ -44,7 +45,7 @@ CSVFilter::EditWidget::EditWidget(CSMWorld::Data& data, QWidget* parent) mHelpAction = new QAction(tr("Help"), this); connect(mHelpAction, &QAction::triggered, this, &EditWidget::openHelp); - mHelpAction->setIcon(QIcon(":/info.png")); + mHelpAction->setIcon(Misc::ScalableIcon::load(":info")); addAction(mHelpAction); auto* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); diff --git a/apps/opencs/view/prefs/keybindingpage.cpp b/apps/opencs/view/prefs/keybindingpage.cpp index d3cc1ff889..f292fa4cf5 100644 --- a/apps/opencs/view/prefs/keybindingpage.cpp +++ b/apps/opencs/view/prefs/keybindingpage.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -61,46 +62,35 @@ namespace CSVPrefs void KeyBindingPage::addSetting(CSMPrefs::Setting* setting) { - std::pair widgets = setting->makeWidgets(this); + const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); - if (widgets.first) + if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { // Label, Option widgets assert(mPageLayout); int next = mPageLayout->rowCount(); - mPageLayout->addWidget(widgets.first, next, 0); - mPageLayout->addWidget(widgets.second, next, 1); + mPageLayout->addWidget(widgets.mLabel, next, 0); + mPageLayout->addWidget(widgets.mInput, next, 1); } - else if (widgets.second) + else if (widgets.mInput != nullptr) { // Wide single widget assert(mPageLayout); int next = mPageLayout->rowCount(); - mPageLayout->addWidget(widgets.second, next, 0, 1, 2); + mPageLayout->addWidget(widgets.mInput, next, 0, 1, 2); } else { - if (setting->getLabel().empty()) - { - // Insert empty space - assert(mPageLayout); + // Create new page + QWidget* pageWidget = new QWidget(); + mPageLayout = new QGridLayout(pageWidget); + mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); - int next = mPageLayout->rowCount(); - mPageLayout->addWidget(new QWidget(), next, 0); - } - else - { - // Create new page - QWidget* pageWidget = new QWidget(); - mPageLayout = new QGridLayout(pageWidget); - mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); + mStackedLayout->addWidget(pageWidget); - mStackedLayout->addWidget(pageWidget); - - mPageSelector->addItem(QString::fromUtf8(setting->getLabel().c_str())); - } + mPageSelector->addItem(setting->getLabel()); } } diff --git a/apps/opencs/view/prefs/page.cpp b/apps/opencs/view/prefs/page.cpp index 4f04a39f00..cc74122782 100644 --- a/apps/opencs/view/prefs/page.cpp +++ b/apps/opencs/view/prefs/page.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "../../model/prefs/category.hpp" #include "../../model/prefs/setting.hpp" @@ -24,21 +25,17 @@ CSVPrefs::Page::Page(CSMPrefs::Category& category, QWidget* parent) void CSVPrefs::Page::addSetting(CSMPrefs::Setting* setting) { - std::pair widgets = setting->makeWidgets(this); + const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); int next = mGrid->rowCount(); - if (widgets.first) + if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { - mGrid->addWidget(widgets.first, next, 0); - mGrid->addWidget(widgets.second, next, 1); + mGrid->addWidget(widgets.mLabel, next, 0); + mGrid->addWidget(widgets.mInput, next, 1); } - else if (widgets.second) + else if (widgets.mInput != nullptr) { - mGrid->addWidget(widgets.second, next, 0, 1, 2); - } - else - { - mGrid->addWidget(new QWidget(this), next, 0); + mGrid->addWidget(widgets.mInput, next, 0, 1, 2); } } diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index d1bfac0ec6..43ac25461e 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -29,7 +30,7 @@ namespace CSVRender Actor::Actor(const ESM::RefId& id, CSMWorld::Data& data) : mId(id) , mData(data) - , mBaseNode(new osg::Group()) + , mBaseNode(new osg::PositionAttitudeTransform()) , mSkeleton(nullptr) { mActorData = mData.getActorAdapter()->getActorData(mId); @@ -46,7 +47,7 @@ namespace CSVRender mBaseNode->removeChildren(0, mBaseNode->getNumChildren()); // Load skeleton - std::string skeletonModel = mActorData->getSkeleton(); + VFS::Path::Normalized skeletonModel = mActorData->getSkeleton(); skeletonModel = Misc::ResourceHelpers::correctActorModelPath(skeletonModel, mData.getResourceSystem()->getVFS()); loadSkeleton(skeletonModel); @@ -60,6 +61,10 @@ namespace CSVRender // Attach parts to skeleton loadBodyParts(); + + const osg::Vec2f& attributes = mActorData->getRaceWeightHeight(); + + mBaseNode->setScale(osg::Vec3d(attributes.x(), attributes.x(), attributes.y())); } else { @@ -85,7 +90,7 @@ namespace CSVRender { auto sceneMgr = mData.getResourceSystem()->getSceneManager(); - osg::ref_ptr temp = sceneMgr->getInstance(model); + osg::ref_ptr temp = sceneMgr->getInstance(VFS::Path::toNormalized(model)); mSkeleton = dynamic_cast(temp.get()); if (!mSkeleton) { @@ -118,7 +123,7 @@ namespace CSVRender auto node = mNodeMap.find(boneName); if (!mesh.empty() && node != mNodeMap.end()) { - auto instance = sceneMgr->getInstance(mesh); + auto instance = sceneMgr->getInstance(VFS::Path::toNormalized(mesh)); SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } diff --git a/apps/opencs/view/render/actor.hpp b/apps/opencs/view/render/actor.hpp index 86c7e7ff2d..09d896e7e7 100644 --- a/apps/opencs/view/render/actor.hpp +++ b/apps/opencs/view/render/actor.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -59,9 +60,9 @@ namespace CSVRender CSMWorld::Data& mData; CSMWorld::ActorAdapter::ActorDataPtr mActorData; - osg::ref_ptr mBaseNode; + osg::ref_ptr mBaseNode; SceneUtil::Skeleton* mSkeleton; - SceneUtil::NodeMapVisitor::NodeMap mNodeMap; + SceneUtil::NodeMap mNodeMap; }; } diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 6238b40e72..3d3b82acf8 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -16,6 +16,7 @@ #include #include +#include "../../model/doc/document.hpp" #include "../../model/world/idtable.hpp" #include "cellarrow.hpp" @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -130,39 +132,31 @@ void CSVRender::Cell::updateLand() return; } - // Setup land if available const CSMWorld::IdCollection& land = mData.getLand(); - int landIndex = land.searchId(mId); - if (landIndex != -1 && !land.getRecord(mId).isDeleted()) + + if (land.getRecord(mId).isDeleted()) + return; + + const ESM::Land& esmLand = land.getRecord(mId).get(); + + if (mTerrain) { - const ESM::Land& esmLand = land.getRecord(mId).get(); - - if (esmLand.getLandData(ESM::Land::DATA_VHGT)) - { - if (mTerrain) - { - mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); - mTerrain->clearAssociatedCaches(); - } - else - { - mTerrain = std::make_unique(mCellNode, mCellNode, mData.getResourceSystem().get(), - mTerrainStorage, Mask_Terrain, ESM::Cell::sDefaultWorldspaceId); - } - - mTerrain->loadCell(esmLand.mX, esmLand.mY); - - if (!mCellBorder) - mCellBorder = std::make_unique(mCellNode, mCoordinates); - - mCellBorder->buildShape(esmLand); - - return; - } + mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); + mTerrain->clearAssociatedCaches(); + } + else + { + constexpr double expiryDelay = 0; + mTerrain = std::make_unique(mCellNode, mCellNode, mData.getResourceSystem().get(), + mTerrainStorage, Mask_Terrain, ESM::Cell::sDefaultWorldspaceId, expiryDelay); } - // No land data - unloadLand(); + mTerrain->loadCell(esmLand.mX, esmLand.mY); + + if (!mCellBorder) + mCellBorder = std::make_unique(mCellNode, mCoordinates); + + mCellBorder->buildShape(esmLand); } void CSVRender::Cell::unloadLand() @@ -174,13 +168,14 @@ void CSVRender::Cell::unloadLand() mCellBorder.reset(); } -CSVRender::Cell::Cell(CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, bool deleted) - : mData(data) +CSVRender::Cell::Cell( + CSMDoc::Document& document, osg::Group* rootNode, const std::string& id, bool deleted, bool isExterior) + : mData(document.getData()) , mId(ESM::RefId::stringRefId(id)) , mDeleted(deleted) , mSubMode(0) , mSubModeElementMask(0) - , mUpdateLand(true) + , mUpdateLand(isExterior) , mLandDeleted(false) { std::pair result = CSMWorld::CellCoordinates::fromId(id); @@ -206,7 +201,17 @@ CSVRender::Cell::Cell(CSMWorld::Data& data, osg::Group* rootNode, const std::str addObjects(0, rows - 1); - updateLand(); + if (mUpdateLand) + { + int landIndex = document.getData().getLand().searchId(mId); + if (landIndex == -1) + { + CSMWorld::IdTable& landTable + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Land)); + document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, mId.getRefIdString())); + } + updateLand(); + } mPathgrid = std::make_unique(mData, mCellNode, mId.getRefIdString(), mCoordinates); mCellWater = std::make_unique(mData, mCellNode, mId.getRefIdString(), mCoordinates); @@ -611,6 +616,30 @@ osg::ref_ptr CSVRender::Cell::getSnapTarget(unsigned int ele return result; } +void CSVRender::Cell::selectFromGroup(const std::vector& group) +{ + for (const auto& [_, object] : mObjects) + { + for (const auto& objectName : group) + { + if (objectName == object->getReferenceId()) + { + object->setSelected(true, osg::Vec4f(1, 0, 1, 1)); + } + } + } +} + +void CSVRender::Cell::unhideAll() +{ + for (const auto& [_, object] : mObjects) + { + osg::ref_ptr rootNode = object->getRootNode(); + if (rootNode->getNodeMask() == Mask_Hidden) + rootNode->setNodeMask(Mask_Reference); + } +} + std::vector> CSVRender::Cell::getSelection(unsigned int elementMask) const { std::vector> result; diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index cf50604c29..5ec8d87c33 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -9,6 +9,7 @@ #include #include +#include "../../model/doc/document.hpp" #include "../../model/world/cellcoordinates.hpp" #include "instancedragmodes.hpp" #include @@ -89,7 +90,8 @@ namespace CSVRender public: /// \note Deleted covers both cells that are deleted and cells that don't exist in /// the first place. - Cell(CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, bool deleted = false); + Cell(CSMDoc::Document& document, osg::Group* rootNode, const std::string& id, bool deleted = false, + bool isExterior = false); ~Cell(); @@ -148,6 +150,10 @@ namespace CSVRender // already selected void selectAllWithSameParentId(int elementMask); + void selectFromGroup(const std::vector& group); + + void unhideAll(); + void handleSelectDrag(Object* object, DragMode dragMode); void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode); diff --git a/apps/opencs/view/render/cellborder.cpp b/apps/opencs/view/render/cellborder.cpp index f63814dcb7..704db13e89 100644 --- a/apps/opencs/view/render/cellborder.cpp +++ b/apps/opencs/view/render/cellborder.cpp @@ -47,9 +47,6 @@ void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) { const ESM::Land::LandData* landData = esmLand.getLandData(ESM::Land::DATA_VHGT); - if (!landData) - return; - mBaseNode->removeChild(mBorderGeometry); mBorderGeometry = new osg::Geometry(); @@ -62,20 +59,40 @@ void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) Traverse the cell border counter-clockwise starting at the SW corner vertex (0, 0). Each loop starts at a corner vertex and ends right before the next corner vertex. */ - for (; x < ESM::Land::LAND_SIZE - 1; ++x) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + if (landData) + { + for (; x < ESM::Land::LAND_SIZE - 1; ++x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - x = ESM::Land::LAND_SIZE - 1; - for (; y < ESM::Land::LAND_SIZE - 1; ++y) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + x = ESM::Land::LAND_SIZE - 1; + for (; y < ESM::Land::LAND_SIZE - 1; ++y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - y = ESM::Land::LAND_SIZE - 1; - for (; x > 0; --x) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + y = ESM::Land::LAND_SIZE - 1; + for (; x > 0; --x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - x = 0; - for (; y > 0; --y) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + x = 0; + for (; y > 0; --y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + } + else + { + for (; x < ESM::Land::LAND_SIZE - 1; ++x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + + x = ESM::Land::LAND_SIZE - 1; + for (; y < ESM::Land::LAND_SIZE - 1; ++y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + + y = ESM::Land::LAND_SIZE - 1; + for (; x > 0; --x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + + x = 0; + for (; y > 0; --y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + } mBorderGeometry->setVertexArray(vertices); diff --git a/apps/opencs/view/render/cellwater.cpp b/apps/opencs/view/render/cellwater.cpp index 7589f2c1fc..5ad860b435 100644 --- a/apps/opencs/view/render/cellwater.cpp +++ b/apps/opencs/view/render/cellwater.cpp @@ -176,14 +176,14 @@ namespace CSVRender mWaterGeometry->setStateSet(SceneUtil::createSimpleWaterStateSet(Alpha, RenderBin)); // Add water texture - std::string textureName = "textures/water/"; - textureName += Fallback::Map::getString("Water_SurfaceTexture"); - textureName += "00.dds"; + constexpr VFS::Path::NormalizedView prefix("textures/water"); + VFS::Path::Normalized texturePath(prefix); + texturePath /= std::string(Fallback::Map::getString("Water_SurfaceTexture")) + "00.dds"; Resource::ImageManager* imageManager = mData.getResourceSystem()->getImageManager(); osg::ref_ptr waterTexture = new osg::Texture2D(); - waterTexture->setImage(imageManager->getImage(textureName)); + waterTexture->setImage(imageManager->getImage(texturePath)); waterTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); waterTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index df5bb02332..7a59222eff 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -40,6 +40,7 @@ #include #include +#include #include "../../model/prefs/shortcut.hpp" #include "../../model/world/commandmacro.hpp" @@ -59,6 +60,24 @@ #include "pagedworldspacewidget.hpp" #include "worldspacewidget.hpp" +namespace +{ + constexpr std::string_view sInstanceModeTooltip = R"( + Instance editing +
  • Use {scene-select-primary} and {scene-select-secondary} to select and unselect instances
  • +
  • Use {scene-edit-primary} to manipulate instances
  • +
  • Use {scene-select-tertiary} to select a reference object and then {scene-edit-secondary} to snap + selection relative to the reference object
  • +
  • Use {scene-submode-move}, {scene-submode-rotate}, {scene-submode-scale} to change to move, rotate, and + scale modes respectively
  • +
  • Use {scene-axis-x}, {scene-axis-y}, and {scene-axis-z} to lock changes to X, Y, and Z axes + respectively
  • +
  • Use {scene-delete} to delete currently selected objects
  • +
  • Use {scene-duplicate} to duplicate instances
  • +
  • Use {scene-instance-drop} to drop instances
+)"; +} + int CSVRender::InstanceMode::getSubModeFromId(const std::string& id) const { return id == "move" ? 0 : (id == "rotate" ? 1 : 2); @@ -186,10 +205,117 @@ osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, con return mousePlanePoint; } +void CSVRender::InstanceMode::saveSelectionGroup(const int group) +{ + QStringList strings; + QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); + QVariant selectionObjects; + CSMWorld::CommandMacro macro(undoStack, "Replace Selection Group"); + std::string groupName = "project::" + std::to_string(group); + + const auto& selection = getWorldspaceWidget().getSelection(Mask_Reference); + const int selectionObjectsIndex + = mSelectionGroups->findColumnIndex(CSMWorld::Columns::ColumnId_SelectionGroupObjects); + + if (dynamic_cast(&getWorldspaceWidget())) + groupName += "-ext"; + else + groupName += "-" + getWorldspaceWidget().getCellId(osg::Vec3f(0, 0, 0)); + + CSMWorld::CreateCommand* newGroup = new CSMWorld::CreateCommand(*mSelectionGroups, groupName); + + newGroup->setType(CSMWorld::UniversalId::Type_SelectionGroup); + + for (const auto& object : selection) + if (const CSVRender::ObjectTag* objectTag = dynamic_cast(object.get())) + strings << QString::fromStdString(objectTag->mObject->getReferenceId()); + + selectionObjects.setValue(strings); + + newGroup->addValue(selectionObjectsIndex, selectionObjects); + + if (mSelectionGroups->getModelIndex(groupName, 0).row() != -1) + macro.push(new CSMWorld::DeleteCommand(*mSelectionGroups, groupName)); + + macro.push(newGroup); + + getWorldspaceWidget().clearSelection(Mask_Reference); +} + +void CSVRender::InstanceMode::getSelectionGroup(const int group) +{ + std::string groupName = "project::" + std::to_string(group); + std::vector targets; + + const auto& selection = getWorldspaceWidget().getSelection(Mask_Reference); + const int selectionObjectsIndex + = mSelectionGroups->findColumnIndex(CSMWorld::Columns::ColumnId_SelectionGroupObjects); + + if (dynamic_cast(&getWorldspaceWidget())) + groupName += "-ext"; + else + groupName += "-" + getWorldspaceWidget().getCellId(osg::Vec3f(0, 0, 0)); + + const QModelIndex groupSearch = mSelectionGroups->getModelIndex(groupName, selectionObjectsIndex); + + if (groupSearch.row() == -1) + return; + + for (const QString& target : groupSearch.data().toStringList()) + targets.push_back(target.toStdString()); + + if (!selection.empty()) + getWorldspaceWidget().clearSelection(Mask_Reference); + + getWorldspaceWidget().selectGroup(targets); +} + +void CSVRender::InstanceMode::setDragAxis(const char axis) +{ + int newDragAxis; + + const std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + + if (selection.empty()) + return; + + switch (axis) + { + case 'x': + newDragAxis = 0; + break; + case 'y': + newDragAxis = 1; + break; + case 'z': + newDragAxis = 2; + break; + default: + return; + } + + if (newDragAxis == mDragAxis) + newDragAxis = -1; + + if (mSubModeId == "move") + { + mObjectsAtDragStart.clear(); + + for (const auto& object : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(object.get())) + { + const osg::Vec3f thisPoint = objectTag->mObject->getPosition().asVec3(); + mDragStart = thisPoint; + mObjectsAtDragStart.emplace_back(thisPoint); + } + } + mDragAxis = newDragAxis; +} + CSVRender::InstanceMode::InstanceMode( WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent) - : EditMode(worldspaceWidget, QIcon(":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, - "Instance editing", parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-instance"), + Mask_Reference | Mask_Terrain, sInstanceModeTooltip.data(), parent) , mSubMode(nullptr) , mSubModeId("move") , mSelectionMode(nullptr) @@ -199,30 +325,42 @@ CSVRender::InstanceMode::InstanceMode( , mUnitScaleDist(1) , mParentNode(std::move(parentNode)) { + mSelectionGroups = dynamic_cast( + worldspaceWidget->getDocument().getData().getTableModel(CSMWorld::UniversalId::Type_SelectionGroup)); + connect(this, &InstanceMode::requestFocus, worldspaceWidget, &WorldspaceWidget::requestFocus); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("scene-delete", worldspaceWidget); - connect( - deleteShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &InstanceMode::deleteSelectedInstances); + connect(deleteShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::deleteSelectedInstances); - // Following classes could be simplified by using QSignalMapper, which is obsolete in Qt5.10, but not in Qt4.8 and - // Qt5.14 - CSMPrefs::Shortcut* dropToCollisionShortcut - = new CSMPrefs::Shortcut("scene-instance-drop-collision", worldspaceWidget); - connect(dropToCollisionShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, - &InstanceMode::dropSelectedInstancesToCollision); - CSMPrefs::Shortcut* dropToTerrainLevelShortcut - = new CSMPrefs::Shortcut("scene-instance-drop-terrain", worldspaceWidget); - connect(dropToTerrainLevelShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, - &InstanceMode::dropSelectedInstancesToTerrain); - CSMPrefs::Shortcut* dropToCollisionShortcut2 - = new CSMPrefs::Shortcut("scene-instance-drop-collision-separately", worldspaceWidget); - connect(dropToCollisionShortcut2, qOverload<>(&CSMPrefs::Shortcut::activated), this, - &InstanceMode::dropSelectedInstancesToCollisionSeparately); - CSMPrefs::Shortcut* dropToTerrainLevelShortcut2 - = new CSMPrefs::Shortcut("scene-instance-drop-terrain-separately", worldspaceWidget); - connect(dropToTerrainLevelShortcut2, qOverload<>(&CSMPrefs::Shortcut::activated), this, - &InstanceMode::dropSelectedInstancesToTerrainSeparately); + CSMPrefs::Shortcut* duplicateShortcut = new CSMPrefs::Shortcut("scene-duplicate", worldspaceWidget); + + connect( + duplicateShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::cloneSelectedInstances); + + connect(new CSMPrefs::Shortcut("scene-instance-drop", worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::dropToCollision); + + for (short i = 0; i <= 9; i++) + { + connect(new CSMPrefs::Shortcut("scene-group-" + std::to_string(i), worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, i] { this->getSelectionGroup(i); }); + connect(new CSMPrefs::Shortcut("scene-save-" + std::to_string(i), worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, i] { this->saveSelectionGroup(i); }); + } + + connect(new CSMPrefs::Shortcut("scene-submode-move", worldspaceWidget), qOverload<>(&CSMPrefs::Shortcut::activated), + this, [this] { mSubMode->setButton("move"); }); + + connect(new CSMPrefs::Shortcut("scene-submode-scale", worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this] { mSubMode->setButton("scale"); }); + + connect(new CSMPrefs::Shortcut("scene-submode-rotate", worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this] { mSubMode->setButton("rotate"); }); + + for (const char axis : "xyz") + connect(new CSMPrefs::Shortcut(std::string("scene-axis-") + axis, worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, axis] { this->setDragAxis(axis); }); } void CSVRender::InstanceMode::activate(CSVWidget::SceneToolbar* toolbar) @@ -1068,7 +1206,7 @@ void CSVRender::InstanceMode::handleSelectDrag(const QPoint& pos) mDragMode = DragMode_None; } -void CSVRender::InstanceMode::deleteSelectedInstances(bool active) +void CSVRender::InstanceMode::deleteSelectedInstances() { std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); if (selection.empty()) @@ -1087,6 +1225,27 @@ void CSVRender::InstanceMode::deleteSelectedInstances(bool active) getWorldspaceWidget().clearSelection(Mask_Reference); } +void CSVRender::InstanceMode::cloneSelectedInstances() +{ + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) + return; + + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& referencesTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_References)); + QUndoStack& undoStack = document.getUndoStack(); + + CSMWorld::CommandMacro macro(undoStack, "Clone Instances"); + for (osg::ref_ptr tag : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) + { + macro.push(new CSMWorld::CloneCommand(referencesTable, objectTag->mObject->getReferenceId(), + "ref#" + std::to_string(referencesTable.rowCount()), CSMWorld::UniversalId::Type_Reference)); + } + // getWorldspaceWidget().clearSelection(Mask_Reference); +} + void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight) { object->setEdited(Object::Override_Position); @@ -1095,7 +1254,7 @@ void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float drop object->setPosition(position.pos); } -float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight) +float CSVRender::InstanceMode::calculateDropHeight(CSVRender::Object* object, float objectHeight) { osg::Vec3d point = object->getPosition().asVec3(); @@ -1109,10 +1268,7 @@ float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender: intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); - if (dropMode & Terrain) - visitor.setTraversalMask(Mask_Terrain); - if (dropMode & Collision) - visitor.setTraversalMask(Mask_Terrain | Mask_Reference); + visitor.setTraversalMask(Mask_Terrain | Mask_Reference); mParentNode->accept(visitor); @@ -1127,27 +1283,7 @@ float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender: return 0.0f; } -void CSVRender::InstanceMode::dropSelectedInstancesToCollision() -{ - handleDropMethod(Collision, "Drop instances to next collision"); -} - -void CSVRender::InstanceMode::dropSelectedInstancesToTerrain() -{ - handleDropMethod(Terrain, "Drop instances to terrain level"); -} - -void CSVRender::InstanceMode::dropSelectedInstancesToCollisionSeparately() -{ - handleDropMethod(CollisionSep, "Drop instances to next collision level separately"); -} - -void CSVRender::InstanceMode::dropSelectedInstancesToTerrainSeparately() -{ - handleDropMethod(TerrainSep, "Drop instances to terrain level separately"); -} - -void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString commandMsg) +void CSVRender::InstanceMode::dropToCollision() { std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); if (selection.empty()) @@ -1156,43 +1292,19 @@ void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString comman CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::CommandMacro macro(undoStack, commandMsg); + CSMWorld::CommandMacro macro(undoStack, "Drop objects to collision"); DropObjectHeightHandler dropObjectDataHandler(&getWorldspaceWidget()); - if (dropMode & Separate) - { - int counter = 0; - for (osg::ref_ptr tag : selection) - if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) - { - float objectHeight = dropObjectDataHandler.mObjectHeights[counter]; - float dropHeight = calculateDropHeight(dropMode, objectTag->mObject, objectHeight); - dropInstance(objectTag->mObject, dropHeight); - objectTag->mObject->apply(macro); - counter++; - } - } - else - { - float smallestDropHeight = std::numeric_limits::max(); - int counter = 0; - for (osg::ref_ptr tag : selection) - if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) - { - float objectHeight = dropObjectDataHandler.mObjectHeights[counter]; - float thisDrop = calculateDropHeight(dropMode, objectTag->mObject, objectHeight); - if (thisDrop < smallestDropHeight) - smallestDropHeight = thisDrop; - counter++; - } - for (osg::ref_ptr tag : selection) - if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) - { - dropInstance(objectTag->mObject, smallestDropHeight); - objectTag->mObject->apply(macro); - } - } + int counter = 0; + for (osg::ref_ptr tag : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) + { + float objectHeight = dropObjectDataHandler.mObjectHeights[counter++]; + float dropHeight = calculateDropHeight(objectTag->mObject, objectHeight); + dropInstance(objectTag->mObject, dropHeight); + objectTag->mObject->apply(macro); + } } CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* worldspacewidget) diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 5055d08d5b..193423efd5 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -14,6 +14,8 @@ #include "editmode.hpp" #include "instancedragmodes.hpp" +#include +#include class QDragEnterEvent; class QDropEvent; @@ -39,17 +41,6 @@ namespace CSVRender { Q_OBJECT - enum DropMode - { - Separate = 0b1, - - Collision = 0b10, - Terrain = 0b100, - - CollisionSep = Collision | Separate, - TerrainSep = Terrain | Separate, - }; - CSVWidget::SceneToolMode* mSubMode; std::string mSubModeId; InstanceSelectionMode* mSelectionMode; @@ -60,6 +51,7 @@ namespace CSVRender osg::ref_ptr mParentNode; osg::Vec3 mDragStart; std::vector mObjectsAtDragStart; + CSMWorld::IdTable* mSelectionGroups; int getSubModeFromId(const std::string& id) const; @@ -74,7 +66,7 @@ namespace CSVRender osg::Vec3 getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart); void handleSelectDrag(const QPoint& pos); void dropInstance(CSVRender::Object* object, float dropHeight); - float calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight); + float calculateDropHeight(CSVRender::Object* object, float objectHeight); osg::Vec3 calculateSnapPositionRelativeToTarget(osg::Vec3 initalPosition, osg::Vec3 targetPosition, osg::Vec3 targetRotation, osg::Vec3 translation, double snap) const; @@ -130,13 +122,13 @@ namespace CSVRender private slots: + void setDragAxis(const char axis); void subModeChanged(const std::string& id); - void deleteSelectedInstances(bool active); - void dropSelectedInstancesToCollision(); - void dropSelectedInstancesToTerrain(); - void dropSelectedInstancesToCollisionSeparately(); - void dropSelectedInstancesToTerrainSeparately(); - void handleDropMethod(DropMode dropMode, QString commandMsg); + void deleteSelectedInstances(); + void cloneSelectedInstances(); + void getSelectionGroup(const int group); + void saveSelectionGroup(const int group); + void dropToCollision(); }; /// \brief Helper class to handle object mask data in safe way diff --git a/apps/opencs/view/render/instancemovemode.cpp b/apps/opencs/view/render/instancemovemode.cpp index e4004a1537..bf0ef9d181 100644 --- a/apps/opencs/view/render/instancemovemode.cpp +++ b/apps/opencs/view/render/instancemovemode.cpp @@ -5,10 +5,12 @@ #include +#include + class QWidget; CSVRender::InstanceMoveMode::InstanceMoveMode(QWidget* parent) - : ModeButton(QIcon(QPixmap(":scenetoolbar/transform-move")), + : ModeButton(Misc::ScalableIcon::load(":scenetoolbar/transform-move"), "Move selected instances" "
  • Use {scene-edit-primary} to move instances around freely
  • " "
  • Use {scene-edit-secondary} to move instances around within the grid
  • " diff --git a/apps/opencs/view/render/instanceselectionmode.cpp b/apps/opencs/view/render/instanceselectionmode.cpp index fa8998747d..d3e2379640 100644 --- a/apps/opencs/view/render/instanceselectionmode.cpp +++ b/apps/opencs/view/render/instanceselectionmode.cpp @@ -58,7 +58,8 @@ namespace CSVRender InstanceSelectionMode::~InstanceSelectionMode() { - mParentNode->removeChild(mBaseNode); + if (mBaseNode) + mParentNode->removeChild(mBaseNode); } void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart) diff --git a/apps/opencs/view/render/mask.hpp b/apps/opencs/view/render/mask.hpp index 818be8b228..1c84d886d3 100644 --- a/apps/opencs/view/render/mask.hpp +++ b/apps/opencs/view/render/mask.hpp @@ -11,11 +11,11 @@ namespace CSVRender enum Mask : unsigned int { // elements that are part of the actual scene - Mask_Reference = 0x2, - Mask_Pathgrid = 0x4, - Mask_Water = 0x8, - Mask_Fog = 0x10, - Mask_Terrain = 0x20, + Mask_Hidden = 0x0, + Mask_Reference = 0x1, + Mask_Pathgrid = 0x2, + Mask_Water = 0x4, + Mask_Terrain = 0x8, // used within models Mask_ParticleSystem = 0x100, diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 7782ce36c9..fe4b6e9b7f 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include @@ -152,7 +151,9 @@ void CSVRender::Object::update() } else if (!model.empty()) { - std::string path = "meshes\\" + model; + constexpr VFS::Path::NormalizedView meshes("meshes"); + VFS::Path::Normalized path(meshes); + path /= model; mResourceSystem->getSceneManager()->getInstance(path, mBaseNode); } else @@ -485,7 +486,7 @@ CSVRender::Object::~Object() mParentNode->removeChild(mRootNode); } -void CSVRender::Object::setSelected(bool selected) +void CSVRender::Object::setSelected(bool selected, const osg::Vec4f& color) { mSelected = selected; @@ -499,7 +500,7 @@ void CSVRender::Object::setSelected(bool selected) mRootNode->removeChild(mBaseNode); if (selected) { - mOutline->setWireframeColor(osg::Vec4f(1, 1, 1, 1)); + mOutline->setWireframeColor(color); mOutline->addChild(mBaseNode); mRootNode->addChild(mOutline); } @@ -714,7 +715,7 @@ void CSVRender::Object::setScale(float scale) { mOverrideFlags |= Override_Scale; - mScaleOverride = scale; + mScaleOverride = std::clamp(scale, 0.5f, 2.0f); adjustTransform(); } diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index 436c410c84..31f0d93ac4 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -5,9 +5,10 @@ #include #include +#include #include -#include +#include #include #include "tagbase.hpp" @@ -138,7 +139,7 @@ namespace CSVRender ~Object(); /// Mark the object as selected, selected objects show an outline effect - void setSelected(bool selected); + void setSelected(bool selected, const osg::Vec4f& color = osg::Vec4f(1, 1, 1, 1)); bool getSelected() const; diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 00d519ecc8..3fd35b7740 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -85,8 +86,8 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() { modified = true; - auto cell = std::make_unique( - mDocument.getData(), mRootNode, iter->first.getId(mWorldspace), deleted); + auto cell + = std::make_unique(mDocument, mRootNode, iter->first.getId(mWorldspace), deleted, true); delete iter->second; iter->second = cell.release(); @@ -160,7 +161,6 @@ void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons(CSVWidget::S { WorldspaceWidget::addVisibilitySelectorButtons(tool); tool->addButton(Button_Terrain, Mask_Terrain, "Terrain"); - tool->addButton(Button_Fog, Mask_Fog, "Fog", "", true); } void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool) @@ -170,9 +170,10 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons(CSVWidget::Sce /// \todo replace EditMode with suitable subclasses tool->addButton(new TerrainShapeMode(this, mRootNode, tool), "terrain-shape"); tool->addButton(new TerrainTextureMode(this, mRootNode, tool), "terrain-texture"); - tool->addButton( - new EditMode(this, QIcon(":placeholder"), Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex"); - tool->addButton(new EditMode(this, QIcon(":placeholder"), Mask_Reference, "Terrain movement"), "terrain-move"); + const QIcon vertexIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-vertex-paint"); + const QIcon movementIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-movement"); + tool->addButton(new EditMode(this, vertexIcon, Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex"); + tool->addButton(new EditMode(this, movementIcon, Mask_Reference, "Terrain movement"), "terrain-move"); } void CSVRender::PagedWorldspaceWidget::handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type) @@ -464,7 +465,7 @@ void CSVRender::PagedWorldspaceWidget::addCellToScene(const CSMWorld::CellCoordi bool deleted = index == -1 || cells.getRecord(index).mState == CSMWorld::RecordBase::State_Deleted; - auto cell = std::make_unique(mDocument.getData(), mRootNode, coordinates.getId(mWorldspace), deleted); + auto cell = std::make_unique(mDocument, mRootNode, coordinates.getId(mWorldspace), deleted, true); EditMode* editMode = getEditMode(); cell->setSubMode(editMode->getSubMode(), editMode->getInteractionMask()); @@ -514,7 +515,7 @@ void CSVRender::PagedWorldspaceWidget::moveCellSelection(int x, int y) addCellToScene(*iter); } - mSelection = newSelection; + mSelection = std::move(newSelection); } void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera(int offsetX, int offsetY) @@ -875,6 +876,18 @@ std::vector> CSVRender::PagedWorldspaceWidget:: return result; } +void CSVRender::PagedWorldspaceWidget::selectGroup(const std::vector& group) const +{ + for (const auto& [_, cell] : mCells) + cell->selectFromGroup(group); +} + +void CSVRender::PagedWorldspaceWidget::unhideAll() const +{ + for (const auto& [_, cell] : mCells) + cell->unhideAll(); +} + std::vector> CSVRender::PagedWorldspaceWidget::getEdited( unsigned int elementMask) const { diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 9ba8911c7e..744cc7ccb9 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -163,6 +163,10 @@ namespace CSVRender std::vector> getSelection(unsigned int elementMask) const override; + void selectGroup(const std::vector& group) const override; + + void unhideAll() const override; + std::vector> getEdited(unsigned int elementMask) const override; void setSubMode(int subMode, unsigned int elementMask) override; diff --git a/apps/opencs/view/render/pathgrid.cpp b/apps/opencs/view/render/pathgrid.cpp index 44e85e7193..7f35956901 100644 --- a/apps/opencs/view/render/pathgrid.cpp +++ b/apps/opencs/view/render/pathgrid.cpp @@ -75,7 +75,7 @@ namespace CSVRender QString text("Pathgrid: "); text += mPathgrid->getId().c_str(); text += " ("; - text += QString::number(SceneUtil::getPathgridNode(static_cast(hit.index0))); + text += QString::number(SceneUtil::getPathgridNode(hit.index0)); text += ")"; return text; diff --git a/apps/opencs/view/render/pathgridmode.cpp b/apps/opencs/view/render/pathgridmode.cpp index 5c45e2b31f..9800f825bc 100644 --- a/apps/opencs/view/render/pathgridmode.cpp +++ b/apps/opencs/view/render/pathgridmode.cpp @@ -2,6 +2,7 @@ #include +#include #include #include "../../model/prefs/state.hpp" @@ -36,8 +37,8 @@ class QWidget; namespace CSVRender { PathgridMode::PathgridMode(WorldspaceWidget* worldspaceWidget, QWidget* parent) - : EditMode(worldspaceWidget, QIcon(":placeholder"), Mask_Pathgrid | Mask_Terrain | Mask_Reference, getTooltip(), - parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-pathgrid"), + Mask_Pathgrid | Mask_Terrain | Mask_Reference, getTooltip(), parent) , mDragMode(DragMode_None) , mFromNode(0) , mSelectionMode(nullptr) @@ -107,7 +108,7 @@ namespace CSVRender { if (tag->getPathgrid()->isSelected()) { - unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); + unsigned short node = SceneUtil::getPathgridNode(hit.index0); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Connect node to selected nodes"; @@ -128,7 +129,7 @@ namespace CSVRender if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { mLastId = tag->getPathgrid()->getId(); - unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); + unsigned short node = SceneUtil::getPathgridNode(hit.index0); tag->getPathgrid()->toggleSelected(node); } } @@ -146,7 +147,7 @@ namespace CSVRender mLastId = tag->getPathgrid()->getId(); } - unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); + unsigned short node = SceneUtil::getPathgridNode(hit.index0); tag->getPathgrid()->toggleSelected(node); return; @@ -189,7 +190,7 @@ namespace CSVRender { mDragMode = DragMode_Edge; mEdgeId = tag->getPathgrid()->getId(); - mFromNode = SceneUtil::getPathgridNode(static_cast(hit.index0)); + mFromNode = SceneUtil::getPathgridNode(hit.index0); tag->getPathgrid()->setDragOrigin(mFromNode); return true; @@ -229,7 +230,7 @@ namespace CSVRender if (hit.tag && (tag = dynamic_cast(hit.tag.get())) && tag->getPathgrid()->getId() == mEdgeId) { - unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); + unsigned short node = SceneUtil::getPathgridNode(hit.index0); cell->getPathgrid()->setDragEndpoint(node); } else @@ -267,7 +268,7 @@ namespace CSVRender { if (tag->getPathgrid()->getId() == mEdgeId) { - unsigned short toNode = SceneUtil::getPathgridNode(static_cast(hit.index0)); + unsigned short toNode = SceneUtil::getPathgridNode(hit.index0); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Add edge between nodes"; diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 12170c86a0..716a087d02 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include "../widget/scenetoolmode.hpp" @@ -76,6 +77,8 @@ namespace CSVRender = new osgViewer::GraphicsWindowEmbedded(0, 0, width(), height()); mWidget->setGraphicsWindowEmbedded(window); + mRenderer->setRealizeOperation(new SceneUtil::GetGLExtensionsOperation()); + int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); mRenderer->setRunMaxFrameRate(frameRateLimit); mRenderer->setUseConfigureAffinity(false); @@ -87,10 +90,10 @@ namespace CSVRender mView->getCamera()->setGraphicsContext(window); - SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager; + osg::ref_ptr lightMgr = new SceneUtil::LightManager; lightMgr->setStartLight(1); lightMgr->setLightingMask(Mask_Lighting); - mRootNode = lightMgr; + mRootNode = std::move(lightMgr); mView->getCamera()->setViewport(new osg::Viewport(0, 0, width(), height())); diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index 6e1a216816..b9dc301efa 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -31,6 +31,7 @@ #include #include +#include #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolshapebrush.hpp" @@ -62,8 +63,8 @@ namespace osg CSVRender::TerrainShapeMode::TerrainShapeMode( WorldspaceWidget* worldspaceWidget, osg::Group* parentNode, QWidget* parent) - : EditMode( - worldspaceWidget, QIcon{ ":scenetoolbar/editing-terrain-shape" }, Mask_Terrain, "Terrain land editing", parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-shape"), Mask_Terrain, + "Terrain land editing", parent) , mParentNode(parentNode) { } @@ -127,7 +128,7 @@ void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& { if (hit.hit && hit.tag == nullptr) { - if (mShapeEditTool == ShapeEditTool_Flatten) + if (mShapeEditTool == ShapeEditTool_Flatten || mShapeEditTool == ShapeEditTool_Equalize) setFlattenToolTargetHeight(hit); if (mDragMode == InteractionType_PrimaryEdit && mShapeEditTool != ShapeEditTool_Drag) { @@ -166,7 +167,7 @@ bool CSVRender::TerrainShapeMode::primaryEditStartDrag(const QPoint& pos) { mEditingPos = hit.worldPos; mIsEditing = true; - if (mShapeEditTool == ShapeEditTool_Flatten) + if (mShapeEditTool == ShapeEditTool_Flatten || mShapeEditTool == ShapeEditTool_Equalize) setFlattenToolTargetHeight(hit); } @@ -462,6 +463,8 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } if (mBrushShape == CSVWidget::BrushShape_Square) @@ -488,6 +491,8 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } } } @@ -528,6 +533,8 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } } } @@ -557,6 +564,8 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } } } @@ -888,6 +897,30 @@ void CSVRender::TerrainShapeMode::flattenHeight( alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight + toolStrength); } +void CSVRender::TerrainShapeMode::equalizeHeight( + const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int targetHeight) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + + float thisHeight = 0.0f; + + const std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + + if (!noCell(cellId) && !noLand(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + + thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; + } + + alterHeight(cellCoords, inCellX, inCellY, targetHeight - thisHeight); +} + void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, float* rightHeight, float* rightAlteredHeight, float* downHeight, diff --git a/apps/opencs/view/render/terrainshapemode.hpp b/apps/opencs/view/render/terrainshapemode.hpp index e772621c4c..2344676cbd 100644 --- a/apps/opencs/view/render/terrainshapemode.hpp +++ b/apps/opencs/view/render/terrainshapemode.hpp @@ -74,7 +74,8 @@ namespace CSVRender ShapeEditTool_PaintToRaise = 1, ShapeEditTool_PaintToLower = 2, ShapeEditTool_Smooth = 3, - ShapeEditTool_Flatten = 4 + ShapeEditTool_Flatten = 4, + ShapeEditTool_Equalize = 5 }; /// Editmode for terrain shape grid @@ -145,6 +146,9 @@ namespace CSVRender void flattenHeight( const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight); + /// Do a single equalize alteration for transient shape edit map + void equalizeHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int targetHeight); + /// Get altered height values around one vertex void updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index 5d2a4874c8..1c494820dc 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -42,14 +41,9 @@ namespace CSVRender land, ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX); } - const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin) { - const int row = mData.getLandTextures().searchId( - ESM::RefId::stringRefId(CSMWorld::LandTexture::createUniqueRecordId(plugin, index))); - if (row == -1) - return nullptr; - - return &mData.getLandTextures().getRecord(row).get(); + return mData.getLandTextures().getLandTexture(index, plugin); } void TerrainStorage::setAlteredHeight(int inCellX, int inCellY, float height) @@ -154,6 +148,8 @@ namespace CSVRender void TerrainStorage::adjustColor(int col, int row, const ESM::LandData* heightData, osg::Vec4ub& color) const { + if (!heightData) + return; // Highlight broken height changes int heightWarningLimit = 1024; if (((col > 0 && row > 0) && leftOrUpIsOverTheLimit(col, row, heightWarningLimit, heightData->getHeights())) diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp index b09de55081..f7a7f72201 100644 --- a/apps/opencs/view/render/terrainstorage.hpp +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -38,7 +38,7 @@ namespace CSVRender std::array mAlteredHeight; osg::ref_ptr getLand(ESM::ExteriorCellLocation cellLocation) override; - const ESM::LandTexture* getLandTexture(int index, short plugin) override; + const std::string* getLandTexture(std::uint16_t index, int plugin) override; void getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) override; diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index 79e9959cd6..cfc7f50cf1 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include "../widget/scenetoolbar.hpp" @@ -37,7 +38,6 @@ #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/landtexture.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" @@ -49,9 +49,8 @@ CSVRender::TerrainTextureMode::TerrainTextureMode( WorldspaceWidget* worldspaceWidget, osg::Group* parentNode, QWidget* parent) - : EditMode(worldspaceWidget, QIcon{ ":scenetoolbar/editing-terrain-texture" }, Mask_Terrain | Mask_Reference, - "Terrain texture editing", parent) - , mBrushTexture("L0#0") + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-texture"), + Mask_Terrain | Mask_Reference, "Terrain texture editing", parent) , mBrushSize(1) , mBrushShape(CSVWidget::BrushShape_Point) , mTextureBrushScenetool(nullptr) @@ -136,8 +135,8 @@ void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult mCellId = getWorldspaceWidget().getCellId(hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mBrushTexture)); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { @@ -185,8 +184,8 @@ bool CSVRender::TerrainTextureMode::primaryEditStartDrag(const QPoint& pos) mDragMode = InteractionType_PrimaryEdit; - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - const int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mBrushTexture)); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { @@ -241,8 +240,8 @@ void CSVRender::TerrainTextureMode::drag(const QPoint& pos, int diffX, int diffY std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - const int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mBrushTexture)); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { @@ -272,8 +271,8 @@ void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - const int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mBrushTexture)); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { @@ -302,18 +301,7 @@ void CSVRender::TerrainTextureMode::handleDropEvent(QDropEvent* event) for (const CSMWorld::UniversalId& uid : ids) { - mBrushTexture = uid.getId(); - emit passBrushTexture(mBrushTexture); - } - } - if (mime->holdsType(CSMWorld::UniversalId::Type_Texture)) - { - const std::vector ids = mime->getData(); - - for (const CSMWorld::UniversalId& uid : ids) - { - std::string textureFileName = uid.toString(); - createTexture(textureFileName); + mBrushTexture = ESM::RefId::stringRefId(uid.getId()); emit passBrushTexture(mBrushTexture); } } @@ -358,11 +346,8 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex); - std::size_t hashlocation = mBrushTexture.find('#'); - std::string mBrushTextureInt = mBrushTexture.substr(hashlocation + 1); - // All indices are offset by +1 - int brushInt = Misc::StringUtils::toNumeric(mBrushTexture.substr(hashlocation + 1), 0) + 1; + uint32_t brushInt = document.getData().getLandTextures().getRecord(mBrushTexture).get().mIndex + 1; int r = static_cast(mBrushSize) / 2; @@ -541,7 +526,7 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe = landTable.data(landTable.getModelIndex(cellId, textureColumn)) .value(); newTerrainOtherCell[yInOtherCell * landTextureSize + xInOtherCell] = brushInt; - pushEditToCommand(newTerrainOtherCell, document, landTable, cellId); + pushEditToCommand(newTerrainOtherCell, document, landTable, std::move(cellId)); } } } @@ -661,50 +646,6 @@ void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColu undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); } -void CSVRender::TerrainTextureMode::createTexture(const std::string& textureFileName) -{ - CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - - CSMWorld::IdTable& ltexTable - = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); - - QUndoStack& undoStack = document.getUndoStack(); - - std::string newId; - - int counter = 0; - bool freeIndexFound = false; - do - { - const size_t maxCounter = std::numeric_limits::max() - 1; - try - { - newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); - if (ltexTable.getRecord(newId).isDeleted() == 0) - counter = (counter + 1) % maxCounter; - } - catch (const std::exception&) - { - newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); - freeIndexFound = true; - } - } while (freeIndexFound == false); - - std::size_t idlocation = textureFileName.find("Texture: "); - QString fileName = QString::fromStdString(textureFileName.substr(idlocation + 9)); - - QVariant textureFileNameVariant; - textureFileNameVariant.setValue(fileName); - - undoStack.beginMacro("Add land texture record"); - - undoStack.push(new CSMWorld::CreateCommand(ltexTable, newId)); - QModelIndex index(ltexTable.getModelIndex(newId, ltexTable.findColumnIndex(CSMWorld::Columns::ColumnId_Texture))); - undoStack.push(new CSMWorld::ModifyCommand(ltexTable, index, textureFileNameVariant)); - undoStack.endMacro(); - mBrushTexture = newId; -} - bool CSVRender::TerrainTextureMode::allowLandTextureEditing(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); @@ -831,7 +772,7 @@ void CSVRender::TerrainTextureMode::setBrushShape(CSVWidget::BrushShape brushSha } } -void CSVRender::TerrainTextureMode::setBrushTexture(std::string brushTexture) +void CSVRender::TerrainTextureMode::setBrushTexture(ESM::RefId brushTexture) { - mBrushTexture = std::move(brushTexture); + mBrushTexture = brushTexture; } diff --git a/apps/opencs/view/render/terraintexturemode.hpp b/apps/opencs/view/render/terraintexturemode.hpp index c4b0b5ffd4..6045f2e26b 100644 --- a/apps/opencs/view/render/terraintexturemode.hpp +++ b/apps/opencs/view/render/terraintexturemode.hpp @@ -117,14 +117,11 @@ namespace CSVRender void pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, std::string cellId); - /// \brief Create new land texture record from texture asset - void createTexture(const std::string& textureFileName); - /// \brief Create new cell and land if needed bool allowLandTextureEditing(const std::string& textureFileName); std::string mCellId; - std::string mBrushTexture; + ESM::RefId mBrushTexture; int mBrushSize; CSVWidget::BrushShape mBrushShape; std::unique_ptr mBrushDraw; @@ -139,13 +136,13 @@ namespace CSVRender const int landTextureSize{ ESM::Land::LAND_TEXTURE_SIZE }; signals: - void passBrushTexture(std::string brushTexture); + void passBrushTexture(ESM::RefId brushTexture); public slots: void handleDropEvent(QDropEvent* event); void setBrushSize(int brushSize); void setBrushShape(CSVWidget::BrushShape brushShape); - void setBrushTexture(std::string brushShape); + void setBrushTexture(ESM::RefId brushShape); }; } diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index fee608b200..a7d8af0a62 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -79,7 +79,7 @@ CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget( update(); - mCell = std::make_unique(document.getData(), mRootNode, mCellId); + mCell = std::make_unique(document, mRootNode, mCellId); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) @@ -127,7 +127,7 @@ bool CSVRender::UnpagedWorldspaceWidget::handleDrop( mCellId = universalIdData.begin()->getId(); - mCell = std::make_unique(getDocument().getData(), mRootNode, mCellId); + mCell = std::make_unique(getDocument(), mRootNode, mCellId); mCamPositionSet = false; mOrbitCamControl->reset(); @@ -199,6 +199,16 @@ std::vector> CSVRender::UnpagedWorldspaceWidget return mCell->getSelection(elementMask); } +void CSVRender::UnpagedWorldspaceWidget::selectGroup(const std::vector& group) const +{ + mCell->selectFromGroup(group); +} + +void CSVRender::UnpagedWorldspaceWidget::unhideAll() const +{ + mCell->unhideAll(); +} + std::vector> CSVRender::UnpagedWorldspaceWidget::getEdited( unsigned int elementMask) const { @@ -337,7 +347,6 @@ void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons(CSVWidget: { WorldspaceWidget::addVisibilitySelectorButtons(tool); tool->addButton(Button_Terrain, Mask_Terrain, "Terrain", "", true); - tool->addButton(Button_Fog, Mask_Fog, "Fog"); } std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 10446354e9..89c916415d 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -93,6 +93,10 @@ namespace CSVRender std::vector> getSelection(unsigned int elementMask) const override; + void selectGroup(const std::vector& group) const override; + + void unhideAll() const override; + std::vector> getEdited(unsigned int elementMask) const override; void setSubMode(int subMode, unsigned int elementMask) override; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 6911f5f043..0a02ae456b 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -50,6 +51,7 @@ #include "cameracontroller.hpp" #include "instancemode.hpp" +#include "mask.hpp" #include "object.hpp" #include "pathgridmode.hpp" @@ -73,6 +75,7 @@ CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidge , mShowToolTips(false) , mToolTipDelay(0) , mInConstructor(true) + , mSelectedNavigationMode(0) { setAcceptDrops(true); @@ -135,6 +138,19 @@ CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidge CSMPrefs::Shortcut* abortShortcut = new CSMPrefs::Shortcut("scene-edit-abort", this); connect(abortShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::abortDrag); + connect(new CSMPrefs::Shortcut("scene-toggle-visibility", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::toggleHiddenInstances); + + connect(new CSMPrefs::Shortcut("scene-unhide-all", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::unhideAll); + + connect(new CSMPrefs::Shortcut("scene-clear-selection", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + [this] { this->clearSelection(Mask_Reference); }); + + CSMPrefs::Shortcut* switchPerspectiveShortcut = new CSMPrefs::Shortcut("scene-cam-cycle", this); + connect(switchPerspectiveShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::cycleNavigationMode); + mInConstructor = false; } @@ -174,11 +190,11 @@ void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { - std::vector> selection = getSelection(~0u); + std::vector> selection = getSelection(Mask_Reference); for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { - if (CSVRender::ObjectTag* objectTag = dynamic_cast(it->get())) + if (CSVRender::ObjectTag* objectTag = static_cast(it->get())) { mOrbitCamControl->setCenter(objectTag->mObject->getPosition().asVec3()); } @@ -210,7 +226,7 @@ CSVWidget::SceneToolMode* CSVRender::WorldspaceWidget::makeNavigationSelector(CS "
  • Hold {free-forward:mod} to speed up movement
  • " "
"); tool->addButton( - new CSVRender::OrbitCameraMode(this, QIcon(":scenetoolbar/orbiting-camera"), + new CSVRender::OrbitCameraMode(this, Misc::ScalableIcon::load(":scenetoolbar/orbiting-camera"), "Orbiting Camera" "
  • Always facing the centre point
  • " "
  • Rotate around the centre point via {orbit-up}, {orbit-left}, {orbit-down}, {orbit-right} or by moving " @@ -224,9 +240,11 @@ CSVWidget::SceneToolMode* CSVRender::WorldspaceWidget::makeNavigationSelector(CS tool), "orbit"); - connect(tool, &CSVWidget::SceneToolMode::modeChanged, this, &WorldspaceWidget::selectNavigationMode); + mCameraMode = tool; - return tool; + connect(mCameraMode, &CSVWidget::SceneToolMode::modeChanged, this, &WorldspaceWidget::selectNavigationMode); + + return mCameraMode; } CSVWidget::SceneToolToggle2* CSVRender::WorldspaceWidget::makeSceneVisibilitySelector(CSVWidget::SceneToolbar* parent) @@ -430,7 +448,7 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( osg::Node* node = *nodeIter; if (osg::ref_ptr tag = dynamic_cast(node->getUserData())) { - WorldspaceHitResult hit = { true, tag, 0, 0, 0, intersection.getWorldIntersectPoint() }; + WorldspaceHitResult hit = { true, std::move(tag), 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; @@ -740,6 +758,44 @@ void CSVRender::WorldspaceWidget::speedMode(bool activate) mSpeedMode = activate; } +void CSVRender::WorldspaceWidget::toggleHiddenInstances() +{ + const std::vector> selection = getSelection(Mask_Reference); + + if (selection.empty()) + return; + + const CSVRender::ObjectTag* firstSelection = static_cast(selection.begin()->get()); + assert(firstSelection != nullptr); + + const CSVRender::Mask firstMask + = firstSelection->mObject->getRootNode()->getNodeMask() == Mask_Hidden ? Mask_Reference : Mask_Hidden; + + for (const auto& object : selection) + if (const auto objectTag = static_cast(object.get())) + objectTag->mObject->getRootNode()->setNodeMask(firstMask); +} + +void CSVRender::WorldspaceWidget::cycleNavigationMode() +{ + switch (++mSelectedNavigationMode) + { + case (CameraMode::FirstPerson): + mCameraMode->setButton("1st"); + break; + case (CameraMode::Orbit): + mCameraMode->setButton("orbit"); + break; + case (CameraMode::Free): + mCameraMode->setButton("free"); + break; + default: + mCameraMode->setButton("1st"); + mSelectedNavigationMode = 0; + break; + } +} + void CSVRender::WorldspaceWidget::handleInteraction(InteractionType type, bool activate) { if (activate) diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 442f4922f0..9a7df38620 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -75,6 +75,7 @@ namespace CSVRender CSMDoc::Document& mDocument; unsigned int mInteractionMask; CSVWidget::SceneToolMode* mEditMode; + CSVWidget::SceneToolMode* mCameraMode; bool mLocked; int mDragMode; bool mDragging; @@ -89,6 +90,7 @@ namespace CSVRender bool mShowToolTips; int mToolTipDelay; bool mInConstructor; + int mSelectedNavigationMode; public: enum DropType @@ -201,6 +203,10 @@ namespace CSVRender virtual std::vector> getSelection(unsigned int elementMask) const = 0; + virtual void selectGroup(const std::vector&) const = 0; + + virtual void unhideAll() const = 0; + virtual std::vector> getEdited(unsigned int elementMask) const = 0; virtual void setSubMode(int subMode, unsigned int elementMask) = 0; @@ -218,8 +224,14 @@ namespace CSVRender Button_Reference = 0x1, Button_Pathgrid = 0x2, Button_Water = 0x4, - Button_Fog = 0x8, - Button_Terrain = 0x10 + Button_Terrain = 0x8 + }; + + enum CameraMode + { + FirstPerson, + Orbit, + Free }; virtual void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool); @@ -237,6 +249,8 @@ namespace CSVRender bool getSpeedMode(); + void cycleNavigationMode(); + private: void dragEnterEvent(QDragEnterEvent* event) override; @@ -300,6 +314,8 @@ namespace CSVRender void speedMode(bool activate); + void toggleHiddenInstances(); + protected slots: void elementSelectionChanged(); diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index f2795d6de9..f11d7489a8 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include #include @@ -94,7 +96,7 @@ void CSVWidget::SceneToolMode::showPanel(const QPoint& position) void CSVWidget::SceneToolMode::addButton(const std::string& icon, const std::string& id, const QString& tooltip) { - ModeButton* button = new ModeButton(QIcon(QPixmap(icon.c_str())), tooltip, mPanel); + ModeButton* button = new ModeButton(Misc::ScalableIcon::load(icon.c_str()), tooltip, mPanel); addButton(button, id); } diff --git a/apps/opencs/view/widget/scenetoolrun.cpp b/apps/opencs/view/widget/scenetoolrun.cpp index 6313f10fa9..59d5bf4d5e 100644 --- a/apps/opencs/view/widget/scenetoolrun.cpp +++ b/apps/opencs/view/widget/scenetoolrun.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include #include @@ -60,7 +62,7 @@ CSVWidget::SceneToolRun::SceneToolRun( , mSelected(mProfiles.begin()) , mToolTip(toolTip) { - setIcon(QIcon(icon)); + setIcon(Misc::ScalableIcon::load(icon)); updateIcon(); adjustToolTips(); diff --git a/apps/opencs/view/widget/scenetoolshapebrush.cpp b/apps/opencs/view/widget/scenetoolshapebrush.cpp index 57b78ffc71..d7034cac07 100644 --- a/apps/opencs/view/widget/scenetoolshapebrush.cpp +++ b/apps/opencs/view/widget/scenetoolshapebrush.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include #include #include @@ -60,10 +62,10 @@ CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidge : QFrame(parent, Qt::Popup) , mDocument(document) { - mButtonPoint = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-point")), "", this); - mButtonSquare = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-square")), "", this); - mButtonCircle = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-circle")), "", this); - mButtonCustom = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-custom")), "", this); + mButtonPoint = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-point"), "", this); + mButtonSquare = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-square"), "", this); + mButtonCircle = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-circle"), "", this); + mButtonCustom = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-custom"), "", this); mSizeSliders = new ShapeBrushSizeControls("Brush size", this); @@ -107,6 +109,7 @@ CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidge mToolSelector->addItem(tr("Height, lower (paint)")); mToolSelector->addItem(tr("Smooth (paint)")); mToolSelector->addItem(tr("Flatten (paint)")); + mToolSelector->addItem(tr("Equalize (paint)")); QLabel* brushStrengthLabel = new QLabel(this); brushStrengthLabel->setText("Brush strength:"); @@ -201,25 +204,25 @@ void CSVWidget::SceneToolShapeBrush::setButtonIcon(CSVWidget::BrushShape brushSh { case BrushShape_Point: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-point"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-point")); tooltip += mShapeBrushWindow->toolTipPoint; break; case BrushShape_Square: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-square"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-square")); tooltip += mShapeBrushWindow->toolTipSquare; break; case BrushShape_Circle: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-circle"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-circle")); tooltip += mShapeBrushWindow->toolTipCircle; break; case BrushShape_Custom: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-custom"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-custom")); tooltip += mShapeBrushWindow->toolTipCustom; break; } diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp index cc372753f6..27d628a384 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.cpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -27,17 +27,19 @@ #include #include #include +#include #include #include #include +#include + #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idcollection.hpp" #include "../../model/world/idtable.hpp" -#include "../../model/world/landtexture.hpp" #include "../../model/world/universalid.hpp" namespace CSVWidget @@ -72,12 +74,12 @@ CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QW : QFrame(parent, Qt::Popup) , mDocument(document) { - mBrushTextureLabel = "Selected texture: " + mBrushTexture + " "; + mBrushTextureLabel = "Selected texture: " + mBrushTexture.getRefIdString() + " "; - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - const int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mBrushTexture)); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { @@ -90,10 +92,10 @@ CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QW mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel)); } - mButtonPoint = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-point")), "", this); - mButtonSquare = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-square")), "", this); - mButtonCircle = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-circle")), "", this); - mButtonCustom = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-custom")), "", this); + mButtonPoint = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-point"), "", this); + mButtonSquare = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-square"), "", this); + mButtonCircle = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-circle"), "", this); + mButtonCustom = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-custom"), "", this); mSizeSliders = new BrushSizeControls("Brush size", this); @@ -152,68 +154,38 @@ void CSVWidget::TextureBrushWindow::configureButtonInitialSettings(QPushButton* button->setCheckable(true); } -void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) +void CSVWidget::TextureBrushWindow::setBrushTexture(ESM::RefId brushTexture) { CSMWorld::IdTable& ltexTable = dynamic_cast( *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); QUndoStack& undoStack = mDocument.getUndoStack(); - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - int index = 0; - int pluginInDragged = 0; - CSMWorld::LandTexture::parseUniqueRecordId(brushTexture, pluginInDragged, index); - const ESM::RefId brushTextureRefId = ESM::RefId::stringRefId(brushTexture); - std::string newBrushTextureId = CSMWorld::LandTexture::createUniqueRecordId(0, index); - ESM::RefId newBrushTextureRefId = ESM::RefId::stringRefId(newBrushTextureId); - int rowInBase = landtexturesCollection.searchId(brushTextureRefId); - int rowInNew = landtexturesCollection.searchId(newBrushTextureRefId); + int row = landtexturesCollection.getIndex(brushTexture); + const auto& record = landtexturesCollection.getRecord(row); - // Check if texture exists in current plugin, and clone if id found in base, otherwise reindex the texture - // TO-DO: Handle case when texture is not found in neither base or plugin properly (finding new index is not enough) - // TO-DO: Handle conflicting plugins properly - if (rowInNew == -1) + if (!record.isDeleted()) { - if (rowInBase == -1) + // Ensure the texture is defined by the current plugin + if (!record.isModified()) { - int counter = 0; - bool freeIndexFound = false; - const int maxCounter = std::numeric_limits::max() - 1; - do - { - newBrushTextureId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); - newBrushTextureRefId = ESM::RefId::stringRefId(newBrushTextureId); - if (landtexturesCollection.searchId(brushTextureRefId) != -1 - && landtexturesCollection.getRecord(brushTextureRefId).isDeleted() == 0 - && landtexturesCollection.searchId(newBrushTextureRefId) != -1 - && landtexturesCollection.getRecord(newBrushTextureRefId).isDeleted() == 0) - counter = (counter + 1) % maxCounter; - else - freeIndexFound = true; - } while (freeIndexFound == false || counter < maxCounter); + CSMWorld::CommandMacro macro(undoStack); + macro.push(new CSMWorld::TouchCommand(ltexTable, brushTexture.getRefIdString())); } - - undoStack.beginMacro("Add land texture record"); - undoStack.push(new CSMWorld::CloneCommand( - ltexTable, brushTexture, newBrushTextureId, CSMWorld::UniversalId::Type_LandTexture)); - undoStack.endMacro(); - } - - if (index != -1 && !landtexturesCollection.getRecord(rowInNew).isDeleted()) - { - mBrushTextureLabel = "Selected texture: " + newBrushTextureId + " "; + mBrushTextureLabel = "Selected texture: " + brushTexture.getRefIdString() + " "; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) - + landtexturesCollection.getData(rowInNew, landTextureFilename).value()); + + landtexturesCollection.getData(row, landTextureFilename).value()); } else { - newBrushTextureId.clear(); + brushTexture = {}; mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); } - mBrushTexture = newBrushTextureId; + mBrushTexture = brushTexture; emit passTextureId(mBrushTexture); emit passBrushShape(mBrushShape); // updates the icon tooltip @@ -248,7 +220,6 @@ CSVWidget::SceneToolTextureBrush::SceneToolTextureBrush( , mTextureBrushWindow(new TextureBrushWindow(document, this)) { mBrushHistory.resize(1); - mBrushHistory[0] = "L0#0"; setAcceptDrops(true); connect(mTextureBrushWindow, &TextureBrushWindow::passBrushShape, this, &SceneToolTextureBrush::setButtonIcon); @@ -282,39 +253,40 @@ void CSVWidget::SceneToolTextureBrush::setButtonIcon(CSVWidget::BrushShape brush { case BrushShape_Point: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-point"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-point")); tooltip += mTextureBrushWindow->toolTipPoint; break; case BrushShape_Square: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-square"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-square")); tooltip += mTextureBrushWindow->toolTipSquare; break; case BrushShape_Circle: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-circle"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-circle")); tooltip += mTextureBrushWindow->toolTipCircle; break; case BrushShape_Custom: - setIcon(QIcon(QPixmap(":scenetoolbar/brush-custom"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-custom")); tooltip += mTextureBrushWindow->toolTipCustom; break; } tooltip += "

    (right click to access of previously used brush settings)"; - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - const int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mTextureBrushWindow->mBrushTexture)); + const int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { - tooltip += "

    Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture) + " "; + tooltip += "

    Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture.getRefIdString()) + + " "; tooltip += landtexturesCollection.getData(index, landTextureFilename).value(); } @@ -340,25 +312,25 @@ void CSVWidget::SceneToolTextureBrush::updatePanel() for (int i = mBrushHistory.size() - 1; i >= 0; --i) { - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - const int index = landtexturesCollection.searchId(ESM::RefId::stringRefId(mBrushHistory[i])); + const int index = landtexturesCollection.searchId(mBrushHistory[i]); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { mTable->setItem(i, 1, new QTableWidgetItem(landtexturesCollection.getData(index, landTextureFilename).value())); - mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i]))); + mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i].getRefIdString()))); } else { mTable->setItem(i, 1, new QTableWidgetItem("Invalid/deleted texture")); - mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i]))); + mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i].getRefIdString()))); } } } -void CSVWidget::SceneToolTextureBrush::updateBrushHistory(const std::string& brushTexture) +void CSVWidget::SceneToolTextureBrush::updateBrushHistory(ESM::RefId brushTexture) { mBrushHistory.insert(mBrushHistory.begin(), brushTexture); if (mBrushHistory.size() > 5) @@ -369,7 +341,7 @@ void CSVWidget::SceneToolTextureBrush::clicked(const QModelIndex& index) { if (index.column() == 0 || index.column() == 1) { - std::string brushTexture = mBrushHistory[index.row()]; + ESM::RefId brushTexture = mBrushHistory[index.row()]; std::swap(mBrushHistory[index.row()], mBrushHistory[0]); mTextureBrushWindow->setBrushTexture(brushTexture); emit passTextureId(brushTexture); diff --git a/apps/opencs/view/widget/scenetooltexturebrush.hpp b/apps/opencs/view/widget/scenetooltexturebrush.hpp index 940ab60ee7..09c7ef43f0 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.hpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.hpp @@ -9,6 +9,8 @@ #include "scenetool.hpp" #endif +#include + class QTableWidget; class QDragEnterEvent; class QDropEvent; @@ -70,7 +72,7 @@ namespace CSVWidget private: CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; int mBrushSize = 1; - std::string mBrushTexture = "L0#0"; + ESM::RefId mBrushTexture; CSMDoc::Document& mDocument; QLabel* mSelectedBrush; QGroupBox* mHorizontalGroupBox; @@ -85,14 +87,14 @@ namespace CSVWidget friend class CSVRender::TerrainTextureMode; public slots: - void setBrushTexture(std::string brushTexture); + void setBrushTexture(ESM::RefId brushTexture); void setBrushShape(); void setBrushSize(int brushSize); signals: void passBrushSize(int brushSize); void passBrushShape(CSVWidget::BrushShape brushShape); - void passTextureId(std::string brushTexture); + void passTextureId(ESM::RefId brushTexture); }; class SceneToolTextureBrush : public SceneTool @@ -103,7 +105,7 @@ namespace CSVWidget CSMDoc::Document& mDocument; QFrame* mPanel; QTableWidget* mTable; - std::vector mBrushHistory; + std::vector mBrushHistory; TextureBrushWindow* mTextureBrushWindow; private: @@ -122,14 +124,14 @@ namespace CSVWidget public slots: void setButtonIcon(CSVWidget::BrushShape brushShape); - void updateBrushHistory(const std::string& mBrushTexture); + void updateBrushHistory(ESM::RefId mBrushTexture); void clicked(const QModelIndex& index); void activate() override; signals: void passEvent(QDropEvent* event); void passEvent(QDragEnterEvent* event); - void passTextureId(std::string brushTexture); + void passTextureId(ESM::RefId brushTexture); }; } diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp index 8dbd1e804c..9e1e8b11c6 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.cpp +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -10,6 +10,8 @@ #include +#include + #include "pushbutton.hpp" #include "scenetoolbar.hpp" @@ -50,7 +52,7 @@ void CSVWidget::SceneToolToggle2::adjustIcon() std::ostringstream stream; stream << mCompositeIcon << buttonIds; - setIcon(QIcon(QString::fromUtf8(stream.str().c_str()))); + setIcon(Misc::ScalableIcon::load(QString::fromUtf8(stream.str().c_str()))); } CSVWidget::SceneToolToggle2::SceneToolToggle2( @@ -87,8 +89,8 @@ void CSVWidget::SceneToolToggle2::addButton( std::ostringstream stream; stream << mSingleIcon << id; - PushButton* button = new PushButton( - QIcon(QPixmap(stream.str().c_str())), PushButton::Type_Toggle, tooltip.isEmpty() ? name : tooltip, mPanel); + PushButton* button = new PushButton(Misc::ScalableIcon::load(stream.str().c_str()), PushButton::Type_Toggle, + tooltip.isEmpty() ? name : tooltip, mPanel); button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize(QSize(mIconSize, mIconSize)); diff --git a/apps/opencs/view/world/datadisplaydelegate.cpp b/apps/opencs/view/world/datadisplaydelegate.cpp index fc6a2b7d80..c642a05e2b 100644 --- a/apps/opencs/view/world/datadisplaydelegate.cpp +++ b/apps/opencs/view/world/datadisplaydelegate.cpp @@ -17,6 +17,8 @@ #include #include +#include + class QModelIndex; class QObject; @@ -39,14 +41,45 @@ CSVWorld::DataDisplayDelegate::DataDisplayDelegate(const ValueList& values, cons , mIconSize(QSize(16, 16)) , mHorizontalMargin(QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) , mTextLeftOffset(8) + , mPixmapsColor(QApplication::palette().text().color()) + , mUiScale(static_cast(QGuiApplication::instance())->devicePixelRatio()) , mSettingKey(pageName + '/' + settingName) { + if (parent) + parent->installEventFilter(this); + buildPixmaps(); if (!pageName.empty()) updateDisplayMode(CSMPrefs::get()[pageName][settingName].toString()); } +bool CSVWorld::DataDisplayDelegate::eventFilter(QObject* target, QEvent* event) +{ + if (event->type() == QEvent::Resize) + { + auto uiScale = static_cast(QGuiApplication::instance())->devicePixelRatio(); + if (mUiScale != uiScale) + { + mUiScale = uiScale; + + buildPixmaps(); + } + } + else if (event->type() == QEvent::PaletteChange) + { + QColor themeColor = QApplication::palette().text().color(); + if (themeColor != mPixmapsColor) + { + mPixmapsColor = std::move(themeColor); + + buildPixmaps(); + } + } + + return false; +} + void CSVWorld::DataDisplayDelegate::buildPixmaps() { if (!mPixmaps.empty()) @@ -161,7 +194,7 @@ void CSVWorld::DataDisplayDelegateFactory::add(int enumValue, const QString& enu Icon icon; icon.mValue = enumValue; icon.mName = enumName; - icon.mIcon = QIcon(iconFilename); + icon.mIcon = Misc::ScalableIcon::load(iconFilename); for (auto it = mIcons.begin(); it != mIcons.end(); ++it) { diff --git a/apps/opencs/view/world/datadisplaydelegate.hpp b/apps/opencs/view/world/datadisplaydelegate.hpp index 087fc2a084..3b3c935d8b 100755 --- a/apps/opencs/view/world/datadisplaydelegate.hpp +++ b/apps/opencs/view/world/datadisplaydelegate.hpp @@ -41,6 +41,7 @@ namespace CSVWorld class DataDisplayDelegate : public EnumDelegate { + Q_OBJECT public: typedef std::vector IconList; typedef std::vector> ValueList; @@ -61,6 +62,8 @@ namespace CSVWorld QSize mIconSize; int mHorizontalMargin; int mTextLeftOffset; + QColor mPixmapsColor; + qreal mUiScale; std::string mSettingKey; @@ -80,6 +83,8 @@ namespace CSVWorld /// offset the horizontal position of the text from the right edge of the icon. Default is 8 pixels. void setTextLeftOffset(int offset); + bool eventFilter(QObject* target, QEvent* event) override; + private: /// update the display mode based on a passed string void updateDisplayMode(const std::string&); diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index 24fe70af94..168e555eae 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -129,30 +130,24 @@ QWidget* CSVWorld::NotEditableSubDelegate::createEditor( /* ==============================DialogueDelegateDispatcherProxy========================================== */ -CSVWorld::DialogueDelegateDispatcherProxy::refWrapper::refWrapper(const QModelIndex& index) - : mIndex(index) -{ -} - CSVWorld::DialogueDelegateDispatcherProxy::DialogueDelegateDispatcherProxy( QWidget* editor, CSMWorld::ColumnBase::Display display) : mEditor(editor) , mDisplay(display) - , mIndexWrapper(nullptr) { } void CSVWorld::DialogueDelegateDispatcherProxy::editorDataCommited() { - if (mIndexWrapper.get()) + if (mIndex.has_value()) { - emit editorDataCommited(mEditor, mIndexWrapper->mIndex, mDisplay); + emit editorDataCommited(mEditor, mIndex.value(), mDisplay); } } void CSVWorld::DialogueDelegateDispatcherProxy::setIndex(const QModelIndex& index) { - mIndexWrapper = std::make_unique(index); + mIndex = index; } QWidget* CSVWorld::DialogueDelegateDispatcherProxy::getEditor() const @@ -655,7 +650,7 @@ void CSVWorld::EditWidget::remake(int row) ++unlocked; } - if (mTable->index(row, i).data().type() == QVariant::UserType) + if (CSMWorld::DisableTag::isDisableTag(mTable->index(row, i).data())) { editor->setEnabled(false); label->setEnabled(false); @@ -705,7 +700,7 @@ void CSVWorld::EditWidget::remake(int row) unlockedLayout->addWidget(editor, unlocked, 1); ++unlocked; - if (tree->index(0, col, tree->index(row, i)).data().type() == QVariant::UserType) + if (CSMWorld::DisableTag::isDisableTag(tree->index(0, col, tree->index(row, i)).data())) { editor->setEnabled(false); label->setEnabled(false); diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index b5108f3b0e..4d05a956c9 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -74,23 +74,14 @@ namespace CSVWorld QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; - // this can't be nested into the DialogueDelegateDispatcher, because it needs to emit signals class DialogueDelegateDispatcherProxy : public QObject { Q_OBJECT - class refWrapper - { - public: - refWrapper(const QModelIndex& index); - - const QModelIndex& mIndex; - }; - QWidget* mEditor; CSMWorld::ColumnBase::Display mDisplay; - std::unique_ptr mIndexWrapper; + std::optional mIndex; public: DialogueDelegateDispatcherProxy(QWidget* editor, CSMWorld::ColumnBase::Display display); diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index ae8c1cb708..a006779ba4 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include "dragdroputils.hpp" void CSVWorld::DragRecordTable::startDragFromTable(const CSVWorld::DragRecordTable& table, const QModelIndex& index) @@ -29,7 +31,7 @@ void CSVWorld::DragRecordTable::startDragFromTable(const CSVWorld::DragRecordTab mime->setIndexAtDragStart(index); QDrag* drag = new QDrag(this); drag->setMimeData(mime); - drag->setPixmap(QString::fromUtf8(mime->getIcon().c_str())); + drag->setPixmap(Misc::ScalableIcon::load(mime->getIcon().c_str()).pixmap(QSize(16, 16))); drag->exec(Qt::CopyAction); } @@ -92,7 +94,7 @@ void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event) if (CSVWorld::DragDropUtils::isTopicOrJournal(*event, display)) { const CSMWorld::TableMimeData* tableMimeData = CSVWorld::DragDropUtils::getTableMimeData(*event); - for (auto universalId : tableMimeData->getData()) + for (const auto& universalId : tableMimeData->getData()) { emit createNewInfoRecord(universalId.getId()); } diff --git a/apps/opencs/view/world/extendedcommandconfigurator.cpp b/apps/opencs/view/world/extendedcommandconfigurator.cpp index 69659be8a6..97494fa076 100644 --- a/apps/opencs/view/world/extendedcommandconfigurator.cpp +++ b/apps/opencs/view/world/extendedcommandconfigurator.cpp @@ -146,7 +146,7 @@ void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vectorfirst->setText(QString::fromUtf8(type.getTypeName().c_str())); current->first->setChecked(true); - current->second = type; + current->second = std::move(type); ++counter; } else diff --git a/apps/opencs/view/world/idcompletiondelegate.cpp b/apps/opencs/view/world/idcompletiondelegate.cpp index 3e26ed9250..e39be392f4 100644 --- a/apps/opencs/view/world/idcompletiondelegate.cpp +++ b/apps/opencs/view/world/idcompletiondelegate.cpp @@ -46,41 +46,41 @@ QWidget* CSVWorld::IdCompletionDelegate::createEditor(QWidget* parent, const QSt switch (conditionFunction) { - case CSMWorld::ConstInfoSelectWrapper::Function_Global: + case ESM::DialogueCondition::Function_Global: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_GlobalVariable); } - case CSMWorld::ConstInfoSelectWrapper::Function_Journal: + case ESM::DialogueCondition::Function_Journal: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Journal); } - case CSMWorld::ConstInfoSelectWrapper::Function_Item: + case ESM::DialogueCondition::Function_Item: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } - case CSMWorld::ConstInfoSelectWrapper::Function_Dead: - case CSMWorld::ConstInfoSelectWrapper::Function_NotId: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_NotId: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotFaction: + case ESM::DialogueCondition::Function_NotFaction: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Faction); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotClass: + case ESM::DialogueCondition::Function_NotClass: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Class); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotRace: + case ESM::DialogueCondition::Function_NotRace: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Race); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotCell: + case ESM::DialogueCondition::Function_NotCell: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Cell); } - case CSMWorld::ConstInfoSelectWrapper::Function_Local: - case CSMWorld::ConstInfoSelectWrapper::Function_NotLocal: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: { return new CSVWidget::DropLineEdit(display, parent); } diff --git a/apps/opencs/view/world/idvalidator.cpp b/apps/opencs/view/world/idvalidator.cpp index 6f790d20cb..078bd6bce5 100644 --- a/apps/opencs/view/world/idvalidator.cpp +++ b/apps/opencs/view/world/idvalidator.cpp @@ -2,17 +2,6 @@ #include -bool CSVWorld::IdValidator::isValid(const QChar& c, bool first) const -{ - if (c.isLetter() || c == '_') - return true; - - if (!first && (c.isDigit() || c.isSpace())) - return true; - - return false; -} - CSVWorld::IdValidator::IdValidator(bool relaxed, QObject* parent) : QValidator(parent) , mRelaxed(relaxed) @@ -92,7 +81,7 @@ QValidator::State CSVWorld::IdValidator::validate(QString& input, int& pos) cons { prevScope = false; - if (!isValid(*iter, first)) + if (!iter->isPrint()) return QValidator::Invalid; } } diff --git a/apps/opencs/view/world/idvalidator.hpp b/apps/opencs/view/world/idvalidator.hpp index e831542961..6b98d35672 100644 --- a/apps/opencs/view/world/idvalidator.hpp +++ b/apps/opencs/view/world/idvalidator.hpp @@ -13,9 +13,6 @@ namespace CSVWorld std::string mNamespace; mutable std::string mError; - private: - bool isValid(const QChar& c, bool first) const; - public: IdValidator(bool relaxed = false, QObject* parent = nullptr); ///< \param relaxed Relaxed rules for IDs that also functino as user visible text diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index f98fe5be5e..5ce83cc849 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -1,11 +1,12 @@ #include "infocreator.hpp" #include +#include #include #include +#include #include -#include #include #include @@ -27,15 +28,27 @@ class QUndoStack; std::string CSVWorld::InfoCreator::getId() const { - const std::string topic = mTopic->text().toStdString(); - - std::string unique = QUuid::createUuid().toByteArray().data(); - - unique.erase(std::remove(unique.begin(), unique.end(), '-'), unique.end()); - - unique = unique.substr(1, unique.size() - 2); - - return topic + '#' + unique; + std::string id = mTopic->text().toStdString(); + size_t length = id.size(); + // We want generated ids to be at most 31 + \0 characters + id.resize(length + 32); + id[length] = '#'; + // Combine a random 32bit number with a random 64bit number for a max 30 character string + quint32 gen32 = QRandomGenerator::global()->generate(); + char* start = id.data() + length + 1; + char* end = start + 10; // 2^32 is a 10 digit number + auto result = std::to_chars(start, end, gen32); + quint64 gen64 = QRandomGenerator::global()->generate64(); + if (gen64) + { + // 0-pad the first number so 10 + 11 isn't the same as 101 + 1 + std::fill(result.ptr, end, '0'); + start = end; + end = start + 20; // 2^64 is a 20 digit number + result = std::to_chars(start, end, gen64); + } + id.resize(result.ptr - id.data()); + return id; } void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const diff --git a/apps/opencs/view/world/landtexturecreator.cpp b/apps/opencs/view/world/landtexturecreator.cpp deleted file mode 100644 index a46e5b6dbe..0000000000 --- a/apps/opencs/view/world/landtexturecreator.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "landtexturecreator.hpp" - -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include "../../model/world/commands.hpp" -#include "../../model/world/idtable.hpp" -#include "../../model/world/landtexture.hpp" - -namespace CSVWorld -{ - LandTextureCreator::LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) - : GenericCreator(data, undoStack, id) - { - // One index is reserved for a default texture - const size_t MaxIndex = std::numeric_limits::max() - 1; - - setManualEditing(false); - - QLabel* nameLabel = new QLabel("Name"); - insertBeforeButtons(nameLabel, false); - - mNameEdit = new QLineEdit(this); - insertBeforeButtons(mNameEdit, true); - - QLabel* indexLabel = new QLabel("Index"); - insertBeforeButtons(indexLabel, false); - - mIndexBox = new QSpinBox(this); - mIndexBox->setMinimum(0); - mIndexBox->setMaximum(MaxIndex); - insertBeforeButtons(mIndexBox, true); - - connect(mNameEdit, &QLineEdit::textChanged, this, &LandTextureCreator::nameChanged); - connect(mIndexBox, qOverload(&QSpinBox::valueChanged), this, &LandTextureCreator::indexChanged); - } - - void LandTextureCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) - { - GenericCreator::cloneMode(originId, type); - - CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); - - int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); - mNameEdit->setText((table.data(table.getModelIndex(originId, column)).toString())); - - column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureIndex); - mIndexBox->setValue((table.data(table.getModelIndex(originId, column)).toInt())); - } - - void LandTextureCreator::focus() - { - mIndexBox->setFocus(); - } - - void LandTextureCreator::reset() - { - GenericCreator::reset(); - mNameEdit->setText(""); - mIndexBox->setValue(0); - } - - std::string LandTextureCreator::getErrors() const - { - if (getData().getLandTextures().searchId(ESM::RefId::stringRefId(getId())) >= 0) - { - return "Index is already in use"; - } - - return ""; - } - - void LandTextureCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const - { - GenericCreator::configureCreateCommand(command); - - CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); - int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); - command.addValue(column, mName.c_str()); - } - - std::string LandTextureCreator::getId() const - { - return CSMWorld::LandTexture::createUniqueRecordId(0, mIndexBox->value()); - } - - void LandTextureCreator::nameChanged(const QString& value) - { - mName = value.toUtf8().constData(); - update(); - } - - void LandTextureCreator::indexChanged(int value) - { - update(); - } -} diff --git a/apps/opencs/view/world/landtexturecreator.hpp b/apps/opencs/view/world/landtexturecreator.hpp deleted file mode 100644 index a2ebf093fb..0000000000 --- a/apps/opencs/view/world/landtexturecreator.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef CSV_WORLD_LANDTEXTURECREATOR_H -#define CSV_WORLD_LANDTEXTURECREATOR_H - -#include - -#include "genericcreator.hpp" - -#include - -namespace CSMWorld -{ - class CreateCommand; - class Data; -} - -class QLineEdit; -class QSpinBox; - -namespace CSVWorld -{ - class LandTextureCreator : public GenericCreator - { - Q_OBJECT - - public: - LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - - void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - - void focus() override; - - void reset() override; - - std::string getErrors() const override; - - protected: - void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - - std::string getId() const override; - - private slots: - - void nameChanged(const QString& val); - void indexChanged(int val); - - private: - QLineEdit* mNameEdit; - QSpinBox* mIndexBox; - - std::string mName; - }; -} - -#endif diff --git a/apps/opencs/view/world/recordbuttonbar.cpp b/apps/opencs/view/world/recordbuttonbar.cpp index 67270bd1ed..e2655136a1 100644 --- a/apps/opencs/view/world/recordbuttonbar.cpp +++ b/apps/opencs/view/world/recordbuttonbar.cpp @@ -12,6 +12,8 @@ #include "../world/tablebottombox.hpp" +#include + #include #include #include @@ -68,12 +70,12 @@ CSVWorld::RecordButtonBar::RecordButtonBar(const CSMWorld::UniversalId& id, CSMW // left section mPrevButton = new QToolButton(this); - mPrevButton->setIcon(QIcon(":record-previous")); + mPrevButton->setIcon(Misc::ScalableIcon::load(":record-previous")); mPrevButton->setToolTip("Switch to previous record"); buttonsLayout->addWidget(mPrevButton, 0); mNextButton = new QToolButton(this); - mNextButton->setIcon(QIcon(":/record-next")); + mNextButton->setIcon(Misc::ScalableIcon::load(":/record-next")); mNextButton->setToolTip("Switch to next record"); buttonsLayout->addWidget(mNextButton, 1); @@ -83,7 +85,7 @@ CSVWorld::RecordButtonBar::RecordButtonBar(const CSMWorld::UniversalId& id, CSMW if (mTable.getFeatures() & CSMWorld::IdTable::Feature_Preview) { QToolButton* previewButton = new QToolButton(this); - previewButton->setIcon(QIcon(":edit-preview")); + previewButton->setIcon(Misc::ScalableIcon::load(":edit-preview")); previewButton->setToolTip("Open a preview of this record"); buttonsLayout->addWidget(previewButton); connect(previewButton, &QToolButton::clicked, this, &RecordButtonBar::showPreview); @@ -92,7 +94,7 @@ CSVWorld::RecordButtonBar::RecordButtonBar(const CSMWorld::UniversalId& id, CSMW if (mTable.getFeatures() & CSMWorld::IdTable::Feature_View) { QToolButton* viewButton = new QToolButton(this); - viewButton->setIcon(QIcon(":/cell.png")); + viewButton->setIcon(Misc::ScalableIcon::load(":cell")); viewButton->setToolTip("Open a scene view of the cell this record is located in"); buttonsLayout->addWidget(viewButton); connect(viewButton, &QToolButton::clicked, this, &RecordButtonBar::viewRecord); @@ -100,22 +102,22 @@ CSVWorld::RecordButtonBar::RecordButtonBar(const CSMWorld::UniversalId& id, CSMW // right section mCloneButton = new QToolButton(this); - mCloneButton->setIcon(QIcon(":edit-clone")); + mCloneButton->setIcon(Misc::ScalableIcon::load(":edit-clone")); mCloneButton->setToolTip("Clone record"); buttonsLayout->addWidget(mCloneButton); mAddButton = new QToolButton(this); - mAddButton->setIcon(QIcon(":edit-add")); + mAddButton->setIcon(Misc::ScalableIcon::load(":edit-add")); mAddButton->setToolTip("Add new record"); buttonsLayout->addWidget(mAddButton); mDeleteButton = new QToolButton(this); - mDeleteButton->setIcon(QIcon(":edit-delete")); + mDeleteButton->setIcon(Misc::ScalableIcon::load(":edit-delete")); mDeleteButton->setToolTip("Delete record"); buttonsLayout->addWidget(mDeleteButton); mRevertButton = new QToolButton(this); - mRevertButton->setIcon(QIcon(":edit-undo")); + mRevertButton->setIcon(Misc::ScalableIcon::load(":edit-undo")); mRevertButton->setToolTip("Revert record"); buttonsLayout->addWidget(mRevertButton); diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index e13648d637..c4e20adf72 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -7,6 +7,8 @@ #include +#include + #include "../../model/world/commands.hpp" #include "../../model/world/universalid.hpp" @@ -38,7 +40,8 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator( { CSMWorld::UniversalId id2(*iter, ""); - mType->addItem(QIcon(id2.getIcon().c_str()), id2.getTypeName().c_str(), static_cast(id2.getType())); + mType->addItem(Misc::ScalableIcon::load(id2.getIcon().c_str()), id2.getTypeName().c_str(), + static_cast(id2.getType())); } mType->model()->sort(0); diff --git a/apps/opencs/view/world/regionmap.cpp b/apps/opencs/view/world/regionmap.cpp index a2847848d0..17d0016afc 100644 --- a/apps/opencs/view/world/regionmap.cpp +++ b/apps/opencs/view/world/regionmap.cpp @@ -224,6 +224,10 @@ CSVWorld::RegionMap::RegionMap(const CSMWorld::UniversalId& universalId, CSMDoc: addAction(mViewInTableAction); setAcceptDrops(true); + + // Make columns square incase QSizeHint doesnt apply + for (int column = 0; column < this->model()->columnCount(); ++column) + this->setColumnWidth(column, this->rowHeight(0)); } void CSVWorld::RegionMap::selectAll() @@ -358,12 +362,23 @@ std::vector CSVWorld::RegionMap::getDraggedRecords() cons return ids; } +void CSVWorld::RegionMap::dragMoveEvent(QDragMoveEvent* event) +{ + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); + if (mime != nullptr && (mime->holdsType(CSMWorld::UniversalId::Type_Region))) + { + event->accept(); + return; + } + + event->ignore(); +} + void CSVWorld::RegionMap::dropEvent(QDropEvent* event) { QModelIndex index = indexAt(event->pos()); bool exists = QTableView::model()->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); - if (!index.isValid() || !exists) { return; diff --git a/apps/opencs/view/world/regionmap.hpp b/apps/opencs/view/world/regionmap.hpp index b6c5078ea3..137b47ed83 100644 --- a/apps/opencs/view/world/regionmap.hpp +++ b/apps/opencs/view/world/regionmap.hpp @@ -59,6 +59,8 @@ namespace CSVWorld void mouseMoveEvent(QMouseEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void dropEvent(QDropEvent* event) override; public: diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index c8505f007f..00acf71235 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -424,6 +425,7 @@ void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent* event) painter.setBackgroundMode(Qt::OpaqueMode); QFont font = painter.font(); QBrush background = painter.background(); + QColor textColor = QApplication::palette().text().color(); while (block.isValid() && top <= event->rect().bottom()) { @@ -440,7 +442,7 @@ void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent* event) else { painter.setBackground(background); - painter.setPen(Qt::black); + painter.setPen(textColor); } painter.setFont(newFont); painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); diff --git a/apps/opencs/view/world/scriptsubview.cpp b/apps/opencs/view/world/scriptsubview.cpp index 793f2e58c1..aa6890904d 100644 --- a/apps/opencs/view/world/scriptsubview.cpp +++ b/apps/opencs/view/world/scriptsubview.cpp @@ -212,7 +212,7 @@ void CSVWorld::ScriptSubView::useHint(const std::string& hint) if (hint.empty()) return; - unsigned line = 0, column = 0; + int line = 0, column = 0; char c; std::istringstream stream(hint.c_str() + 1); switch (hint[0]) @@ -222,8 +222,8 @@ void CSVWorld::ScriptSubView::useHint(const std::string& hint) { QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); QString source = mModel->data(index).toString(); - unsigned stringSize = source.length(); - unsigned pos, dummy; + int stringSize = static_cast(source.length()); + int pos, dummy; if (!(stream >> c >> dummy >> pos)) return; @@ -234,7 +234,7 @@ void CSVWorld::ScriptSubView::useHint(const std::string& hint) pos = stringSize; } - for (unsigned i = 0; i <= pos; ++i) + for (int i = 0; i <= pos; ++i) { if (source[i] == '\n') { diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 0e1f34d2dd..8a47f95e63 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -10,7 +10,6 @@ #include "globalcreator.hpp" #include "infocreator.hpp" #include "landcreator.hpp" -#include "landtexturecreator.hpp" #include "pathgridcreator.hpp" #include "previewsubview.hpp" #include "referenceablecreator.hpp" @@ -92,7 +91,7 @@ void CSVWorld::addSubViewFactories(CSVDoc::SubViewFactoryManager& manager) new CSVDoc::SubViewFactoryWithCreator>); manager.add(CSMWorld::UniversalId::Type_LandTextures, - new CSVDoc::SubViewFactoryWithCreator>); + new CSVDoc::SubViewFactoryWithCreator>); manager.add(CSMWorld::UniversalId::Type_Globals, new CSVDoc::SubViewFactoryWithCreator>); @@ -195,7 +194,7 @@ void CSVWorld::addSubViewFactories(CSVDoc::SubViewFactoryManager& manager) new CSVDoc::SubViewFactoryWithCreator>(false)); manager.add(CSMWorld::UniversalId::Type_LandTexture, - new CSVDoc::SubViewFactoryWithCreator>(false)); + new CSVDoc::SubViewFactoryWithCreator>(false)); manager.add(CSMWorld::UniversalId::Type_DebugProfile, new CSVDoc::SubViewFactoryWithCreator #include +#include #include #include "../../model/doc/document.hpp" @@ -323,7 +324,7 @@ CSVWorld::Table::Table(const CSMWorld::UniversalId& id, bool createAndDelete, bo mEditAction = new QAction(tr("Edit Record"), this); connect(mEditAction, &QAction::triggered, this, &Table::editRecord); - mEditAction->setIcon(QIcon(":edit-edit")); + mEditAction->setIcon(Misc::ScalableIcon::load(":edit-edit")); addAction(mEditAction); CSMPrefs::Shortcut* editShortcut = new CSMPrefs::Shortcut("table-edit", this); editShortcut->associateAction(mEditAction); @@ -332,14 +333,14 @@ CSVWorld::Table::Table(const CSMWorld::UniversalId& id, bool createAndDelete, bo { mCreateAction = new QAction(tr("Add Record"), this); connect(mCreateAction, &QAction::triggered, this, &Table::createRequest); - mCreateAction->setIcon(QIcon(":edit-add")); + mCreateAction->setIcon(Misc::ScalableIcon::load(":edit-add")); addAction(mCreateAction); CSMPrefs::Shortcut* createShortcut = new CSMPrefs::Shortcut("table-add", this); createShortcut->associateAction(mCreateAction); mCloneAction = new QAction(tr("Clone Record"), this); connect(mCloneAction, &QAction::triggered, this, &Table::cloneRecord); - mCloneAction->setIcon(QIcon(":edit-clone")); + mCloneAction->setIcon(Misc::ScalableIcon::load(":edit-clone")); addAction(mCloneAction); CSMPrefs::Shortcut* cloneShortcut = new CSMPrefs::Shortcut("table-clone", this); cloneShortcut->associateAction(mCloneAction); @@ -349,7 +350,7 @@ CSVWorld::Table::Table(const CSMWorld::UniversalId& id, bool createAndDelete, bo { mTouchAction = new QAction(tr("Touch Record"), this); connect(mTouchAction, &QAction::triggered, this, &Table::touchRecord); - mTouchAction->setIcon(QIcon(":edit-touch")); + mTouchAction->setIcon(Misc::ScalableIcon::load(":edit-touch")); addAction(mTouchAction); CSMPrefs::Shortcut* touchShortcut = new CSMPrefs::Shortcut("table-touch", this); touchShortcut->associateAction(mTouchAction); @@ -357,56 +358,56 @@ CSVWorld::Table::Table(const CSMWorld::UniversalId& id, bool createAndDelete, bo mRevertAction = new QAction(tr("Revert Record"), this); connect(mRevertAction, &QAction::triggered, mDispatcher, &CSMWorld::CommandDispatcher::executeRevert); - mRevertAction->setIcon(QIcon(":edit-undo")); + mRevertAction->setIcon(Misc::ScalableIcon::load(":edit-undo")); addAction(mRevertAction); CSMPrefs::Shortcut* revertShortcut = new CSMPrefs::Shortcut("table-revert", this); revertShortcut->associateAction(mRevertAction); mDeleteAction = new QAction(tr("Delete Record"), this); connect(mDeleteAction, &QAction::triggered, mDispatcher, &CSMWorld::CommandDispatcher::executeDelete); - mDeleteAction->setIcon(QIcon(":edit-delete")); + mDeleteAction->setIcon(Misc::ScalableIcon::load(":edit-delete")); addAction(mDeleteAction); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("table-remove", this); deleteShortcut->associateAction(mDeleteAction); mMoveUpAction = new QAction(tr("Move Up"), this); connect(mMoveUpAction, &QAction::triggered, this, &Table::moveUpRecord); - mMoveUpAction->setIcon(QIcon(":record-up")); + mMoveUpAction->setIcon(Misc::ScalableIcon::load(":record-up")); addAction(mMoveUpAction); CSMPrefs::Shortcut* moveUpShortcut = new CSMPrefs::Shortcut("table-moveup", this); moveUpShortcut->associateAction(mMoveUpAction); mMoveDownAction = new QAction(tr("Move Down"), this); connect(mMoveDownAction, &QAction::triggered, this, &Table::moveDownRecord); - mMoveDownAction->setIcon(QIcon(":record-down")); + mMoveDownAction->setIcon(Misc::ScalableIcon::load(":record-down")); addAction(mMoveDownAction); CSMPrefs::Shortcut* moveDownShortcut = new CSMPrefs::Shortcut("table-movedown", this); moveDownShortcut->associateAction(mMoveDownAction); mViewAction = new QAction(tr("View"), this); connect(mViewAction, &QAction::triggered, this, &Table::viewRecord); - mViewAction->setIcon(QIcon(":/cell.png")); + mViewAction->setIcon(Misc::ScalableIcon::load(":cell")); addAction(mViewAction); CSMPrefs::Shortcut* viewShortcut = new CSMPrefs::Shortcut("table-view", this); viewShortcut->associateAction(mViewAction); mPreviewAction = new QAction(tr("Preview"), this); connect(mPreviewAction, &QAction::triggered, this, &Table::previewRecord); - mPreviewAction->setIcon(QIcon(":edit-preview")); + mPreviewAction->setIcon(Misc::ScalableIcon::load(":edit-preview")); addAction(mPreviewAction); CSMPrefs::Shortcut* previewShortcut = new CSMPrefs::Shortcut("table-preview", this); previewShortcut->associateAction(mPreviewAction); mExtendedDeleteAction = new QAction(tr("Extended Delete Record"), this); connect(mExtendedDeleteAction, &QAction::triggered, this, &Table::executeExtendedDelete); - mExtendedDeleteAction->setIcon(QIcon(":edit-delete")); + mExtendedDeleteAction->setIcon(Misc::ScalableIcon::load(":edit-delete")); addAction(mExtendedDeleteAction); CSMPrefs::Shortcut* extendedDeleteShortcut = new CSMPrefs::Shortcut("table-extendeddelete", this); extendedDeleteShortcut->associateAction(mExtendedDeleteAction); mExtendedRevertAction = new QAction(tr("Extended Revert Record"), this); connect(mExtendedRevertAction, &QAction::triggered, this, &Table::executeExtendedRevert); - mExtendedRevertAction->setIcon(QIcon(":edit-undo")); + mExtendedRevertAction->setIcon(Misc::ScalableIcon::load(":edit-undo")); addAction(mExtendedRevertAction); CSMPrefs::Shortcut* extendedRevertShortcut = new CSMPrefs::Shortcut("table-extendedrevert", this); extendedRevertShortcut->associateAction(mExtendedRevertAction); @@ -417,7 +418,7 @@ CSVWorld::Table::Table(const CSMWorld::UniversalId& id, bool createAndDelete, bo mHelpAction = new QAction(tr("Help"), this); connect(mHelpAction, &QAction::triggered, this, &Table::openHelp); - mHelpAction->setIcon(QIcon(":/info.png")); + mHelpAction->setIcon(Misc::ScalableIcon::load(":info")); addAction(mHelpAction); CSMPrefs::Shortcut* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); @@ -694,10 +695,10 @@ void CSVWorld::Table::previewRecord() if (selectedRows.size() == 1) { - std::string id = getUniversalId(selectedRows.begin()->row()).getId(); + CSMWorld::UniversalId id = getUniversalId(selectedRows.begin()->row()); QModelIndex index - = mModel->getModelIndex(id, mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); + = mModel->getModelIndex(id.getId(), mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); if (mModel->data(index) != CSMWorld::RecordBase::State_Deleted) emit editRequest(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, id), ""); diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index 72b70b8109..396ba2de33 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -15,6 +15,11 @@ #include "creator.hpp" #include "infocreator.hpp" +namespace +{ + constexpr const char* statusBarStyle = "QStatusBar::item { border: 0px }"; +} + void CSVWorld::TableBottomBox::updateSize() { // Make sure that the size of the bottom box is determined by the currently visible widget @@ -104,6 +109,7 @@ CSVWorld::TableBottomBox::TableBottomBox(const CreatorFactoryBase& creatorFactor mStatusBar = new QStatusBar(this); mStatusBar->addWidget(mStatus); + mStatusBar->setStyleSheet(statusBarStyle); mLayout->addWidget(mStatusBar); @@ -129,6 +135,18 @@ CSVWorld::TableBottomBox::TableBottomBox(const CreatorFactoryBase& creatorFactor updateSize(); } +bool CSVWorld::TableBottomBox::event(QEvent* event) +{ + // Apply style sheet again if style was changed + if (event->type() == QEvent::PaletteChange) + { + if (mStatusBar != nullptr) + mStatusBar->setStyleSheet(statusBarStyle); + } + + return QWidget::event(event); +} + void CSVWorld::TableBottomBox::setEditLock(bool locked) { if (mCreator) diff --git a/apps/opencs/view/world/tablebottombox.hpp b/apps/opencs/view/world/tablebottombox.hpp index 7be57066f6..193ca33026 100644 --- a/apps/opencs/view/world/tablebottombox.hpp +++ b/apps/opencs/view/world/tablebottombox.hpp @@ -61,6 +61,9 @@ namespace CSVWorld void extendedConfigRequest(ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds); + protected: + bool event(QEvent* event) override; + public: TableBottomBox(const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget* parent = nullptr); diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index c9e09e2d6a..d2bf425ddd 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -17,10 +17,13 @@ #include +#include + #include "../../model/doc/document.hpp" #include "../../model/world/tablemimedata.hpp" #include "../doc/sizehint.hpp" +#include "../doc/view.hpp" #include "../filter/filterbox.hpp" #include "../filter/filterdata.hpp" #include "table.hpp" @@ -60,7 +63,7 @@ CSVWorld::TableSubView::TableSubView( mOptions->hide(); QPushButton* opt = new QPushButton(); - opt->setIcon(QIcon(":startup/configure")); + opt->setIcon(Misc::ScalableIcon::load(":startup/configure")); opt->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); opt->setToolTip("Open additional options for this subview."); connect(opt, &QPushButton::clicked, this, &TableSubView::toggleOptions); @@ -78,8 +81,11 @@ CSVWorld::TableSubView::TableSubView( widget->setLayout(layout); setWidget(widget); + + QScreen* screen = CSVDoc::View::getWidgetScreen(pos()); + // prefer height of the screen and full width of the table - const QRect rect = QApplication::screenAt(pos())->geometry(); + const QRect rect = screen->geometry(); int frameHeight = 40; // set a reasonable default QWidget* topLevel = QApplication::topLevelAt(pos()); if (topLevel) @@ -169,7 +175,7 @@ void CSVWorld::TableSubView::createFilterRequest(std::vectorgetId(); - filterData.columns = col; + filterData.columns = std::move(col); sourceFilter.emplace_back(filterData); } @@ -195,7 +201,7 @@ void CSVWorld::TableSubView::createFilterRequest(std::vector ) diff --git a/apps/opencs_tests/main.cpp b/apps/opencs_tests/main.cpp index e1a8e67397..fd7d4900c8 100644 --- a/apps/opencs_tests/main.cpp +++ b/apps/opencs_tests/main.cpp @@ -1,7 +1,11 @@ +#include + #include int main(int argc, char* argv[]) { + Log::sMinDebugLevel = Debug::getDebugLevel(); + testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/apps/opencs_tests/model/world/testuniversalid.cpp b/apps/opencs_tests/model/world/testuniversalid.cpp index 2e610b0dd0..871a5218d4 100644 --- a/apps/opencs_tests/model/world/testuniversalid.cpp +++ b/apps/opencs_tests/model/world/testuniversalid.cpp @@ -149,39 +149,36 @@ namespace CSMWorld Params{ UniversalId(UniversalId::Type_None), UniversalId::Type_None, UniversalId::Class_None, UniversalId::ArgumentType_None, "-", "-", ":placeholder" }, Params{ UniversalId(UniversalId::Type_RegionMap), UniversalId::Type_RegionMap, UniversalId::Class_NonRecord, - UniversalId::ArgumentType_None, "Region Map", "Region Map", ":./region-map.png" }, + UniversalId::ArgumentType_None, "Region Map", "Region Map", ":region-map" }, Params{ UniversalId(UniversalId::Type_RunLog), UniversalId::Type_RunLog, UniversalId::Class_Transient, - UniversalId::ArgumentType_None, "Run Log", "Run Log", ":./run-log.png" }, + UniversalId::ArgumentType_None, "Run Log", "Run Log", ":run-log" }, Params{ UniversalId(UniversalId::Type_Lands), UniversalId::Type_Lands, UniversalId::Class_RecordList, - UniversalId::ArgumentType_None, "Lands", "Lands", ":./land-heightmap.png" }, + UniversalId::ArgumentType_None, "Lands", "Lands", ":land-heightmap" }, Params{ UniversalId(UniversalId::Type_Icons), UniversalId::Type_Icons, UniversalId::Class_ResourceList, - UniversalId::ArgumentType_None, "Icons", "Icons", ":./resources-icon" }, + UniversalId::ArgumentType_None, "Icons", "Icons", ":resources-icon" }, Params{ UniversalId(UniversalId::Type_Activator, "a"), UniversalId::Type_Activator, - UniversalId::Class_RefRecord, UniversalId::ArgumentType_Id, "Activator", "Activator: a", - ":./activator.png" }, + UniversalId::Class_RefRecord, UniversalId::ArgumentType_Id, "Activator", "Activator: a", ":activator" }, Params{ UniversalId(UniversalId::Type_Gmst, "b"), UniversalId::Type_Gmst, UniversalId::Class_Record, - UniversalId::ArgumentType_Id, "Game Setting", "Game Setting: b", ":./gmst.png" }, + UniversalId::ArgumentType_Id, "Game Setting", "Game Setting: b", ":gmst" }, Params{ UniversalId(UniversalId::Type_Mesh, "c"), UniversalId::Type_Mesh, UniversalId::Class_Resource, - UniversalId::ArgumentType_Id, "Mesh", "Mesh: c", ":./resources-mesh" }, + UniversalId::ArgumentType_Id, "Mesh", "Mesh: c", ":resources-mesh" }, Params{ UniversalId(UniversalId::Type_Scene, "d"), UniversalId::Type_Scene, UniversalId::Class_Collection, - UniversalId::ArgumentType_Id, "Scene", "Scene: d", ":./scene.png" }, + UniversalId::ArgumentType_Id, "Scene", "Scene: d", ":scene" }, Params{ UniversalId(UniversalId::Type_Reference, "e"), UniversalId::Type_Reference, - UniversalId::Class_SubRecord, UniversalId::ArgumentType_Id, "Instance", "Instance: e", - ":./instance.png" }, + UniversalId::Class_SubRecord, UniversalId::ArgumentType_Id, "Instance", "Instance: e", ":instance" }, Params{ UniversalId(UniversalId::Type_Search, 42), UniversalId::Type_Search, UniversalId::Class_Transient, - UniversalId::ArgumentType_Index, "Global Search", "Global Search: 42", ":./menu-search.png" }, + UniversalId::ArgumentType_Index, "Global Search", "Global Search: 42", ":menu-search" }, Params{ UniversalId("Instance: f"), UniversalId::Type_Reference, UniversalId::Class_SubRecord, - UniversalId::ArgumentType_Id, "Instance", "Instance: f", ":./instance.png" }, + UniversalId::ArgumentType_Id, "Instance", "Instance: f", ":instance" }, Params{ UniversalId(UniversalId::Type_Reference, ESM::RefId::stringRefId("g")), UniversalId::Type_Reference, - UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", "Instance: \"g\"", - ":./instance.png" }, + UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", "Instance: g", ":instance" }, Params{ UniversalId(UniversalId::Type_Reference, ESM::RefId::index(ESM::REC_SKIL, 42)), UniversalId::Type_Reference, UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", - "Instance: Index:SKIL:0x2a", ":./instance.png" }, + "Instance: SKIL:0x2a", ":instance" }, }; INSTANTIATE_TEST_SUITE_P(ValidParams, CSMWorldUniversalIdValidPerTypeTest, ValuesIn(validParams)); diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 3df00f1be0..37de0abeab 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -1,29 +1,29 @@ -# local files -set(GAME - main.cpp +set(OPENMW_SOURCES engine.cpp options.cpp +) +set(OPENMW_RESOURCES ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ${CMAKE_SOURCE_DIR}/files/windows/openmw.exe.manifest ) -if (ANDROID) - set(GAME ${GAME} android_main.cpp) -endif() - -set(GAME_HEADER +set(OPENMW_HEADERS + doc.hpp engine.hpp + options.hpp + profile.hpp ) -source_group(game FILES ${GAME} ${GAME_HEADER}) +source_group(apps/openmw FILES main.cpp android_main.cpp ${OPENMW_SOURCES} ${OPENMW_HEADERS} ${OPENMW_RESOURCES}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation vismask + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover - postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass navmeshmode precipitationocclusion ripples + postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples + actorutil distortion animationpriority bonegroup blendmask animblendcontroller ) add_openmw_dir (mwinput @@ -60,15 +60,19 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant - context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings - camerabindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings - types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal - worker magicbindings + context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings + mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings dialoguebindings + postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker magicbindings factionbindings + classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings markupbindings + types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc + types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus + types/potion types/ingredient types/misc types/repair types/armor types/light types/static + types/clothing types/levelledlist types/terminal ) add_openmw_dir (mwsound soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output - loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater volumesettings + loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater ) add_openmw_dir (mwworld @@ -79,18 +83,19 @@ add_openmw_dir (mwworld store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager groundcoverstore magiceffects cell ptrregistry + positioncellgrid ) add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback - contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile + contacttestresultcallback stepper movementsolver projectile actorconvexcallback raycasting mtphysics contacttestwrapper projectileconvexcallback ) add_openmw_dir (mwclass classes activator creature npc weapon armor potion apparatus book clothing container door ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart - esm4base light4 + esm4base esm4npc light4 ) add_openmw_dir (mwmechanics @@ -98,7 +103,7 @@ add_openmw_dir (mwmechanics drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction summoning - character actors objects aistate trading weaponpriority spellpriority weapontype spellutil + character actors objects aistate weaponpriority spellpriority weapontype spellutil spelleffects ) @@ -108,24 +113,32 @@ add_openmw_dir (mwstate add_openmw_dir (mwbase environment world scriptmanager dialoguemanager journal soundmanager mechanicsmanager - inputmanager windowmanager statemanager + inputmanager windowmanager statemanager luamanager ) # Main executable -if (NOT ANDROID) - openmw_add_executable(openmw - ${OPENMW_FILES} - ${GAME} ${GAME_HEADER} - ${APPLE_BUNDLE_RESOURCES} - ) -else () - add_library(openmw - SHARED - ${OPENMW_FILES} - ${GAME} ${GAME_HEADER} - ) -endif () +add_library(openmw-lib STATIC + ${OPENMW_FILES} + ${OPENMW_SOURCES} +) + +if(BUILD_OPENMW) + if (ANDROID) + add_library(openmw SHARED + main.cpp + android_main.cpp + ) + else() + openmw_add_executable(openmw + ${APPLE_BUNDLE_RESOURCES} + ${OPENMW_RESOURCES} + main.cpp + ) + endif() + + target_link_libraries(openmw openmw-lib) +endif() # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING # when we change the backend. @@ -133,7 +146,7 @@ include_directories( ${FFmpeg_INCLUDE_DIRS} ) -target_link_libraries(openmw +target_link_libraries(openmw-lib # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`. @@ -145,19 +158,19 @@ target_link_libraries(openmw ${OSGDB_LIBRARIES} ${OSGUTIL_LIBRARIES} ${OSG_LIBRARIES} - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options ${OPENAL_LIBRARY} ${FFmpeg_LIBRARIES} ${MyGUI_LIBRARIES} - ${SDL2_LIBRARY} + SDL2::SDL2 ${RecastNavigation_LIBRARIES} "osg-ffmpeg-videoplayer" "oics" components ) -if (MSVC) - target_precompile_headers(openmw PRIVATE +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-lib PRIVATE @@ -182,24 +195,26 @@ if (MSVC) ) endif() +add_definitions(-DMYGUI_DONT_USE_OBSOLETE=ON) + if (ANDROID) - target_link_libraries(openmw EGL android log z) + target_link_libraries(openmw-lib EGL android log z) endif (ANDROID) if (USE_SYSTEM_TINYXML) - target_link_libraries(openmw ${TinyXML_LIBRARIES}) + target_link_libraries(openmw-lib ${TinyXML_LIBRARIES}) endif() if (NOT UNIX) - target_link_libraries(openmw ${SDL2MAIN_LIBRARY}) + target_link_libraries(openmw-lib ${SDL2MAIN_LIBRARY}) endif() # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) - target_link_libraries(openmw ${CMAKE_THREAD_LIBS_INIT}) + target_link_libraries(openmw-lib ${CMAKE_THREAD_LIBS_INIT}) endif() -if(APPLE) +if(APPLE AND BUILD_OPENMW) set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources") set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR}) @@ -225,13 +240,17 @@ if(APPLE) "LINKER:SHELL:-framework AudioToolbox" "LINKER:SHELL:-framework VideoDecodeAcceleration") endif() -endif(APPLE) - -if (BUILD_WITH_CODE_COVERAGE) - target_compile_options(openmw PRIVATE --coverage) - target_link_libraries(openmw gcov) endif() -if (WIN32) +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw-lib PRIVATE --coverage) + target_link_libraries(openmw-lib gcov) + if (NOT ANDROID AND BUILD_OPENMW) + target_compile_options(openmw PRIVATE --coverage) + target_link_libraries(openmw gcov) + endif() +endif() + +if (WIN32 AND BUILD_OPENMW) INSTALL(TARGETS openmw RUNTIME DESTINATION ".") -endif (WIN32) +endif() diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 0c77329f48..2736f339e4 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -30,6 +31,7 @@ #include #include +#include #include #include @@ -62,6 +64,7 @@ #include "mwscript/interpretercontext.hpp" #include "mwscript/scriptmanagerimp.hpp" +#include "mwsound/constants.hpp" #include "mwsound/soundmanagerimp.hpp" #include "mwworld/class.hpp" @@ -105,14 +108,27 @@ namespace }); // the forEachUserStatsValue loop is "run" at compile time, hence the settings manager is not available. // Unconditionnally add the async physics stats, and then remove it at runtime if necessary - if (Settings::Manager::getInt("async num threads", "Physics") == 0) + if (Settings::physics().mAsyncNumThreads == 0) profiler.removeUserStatsLine(" -Async"); } - struct ScheduleNonDialogMessageBox + struct ScreenCaptureMessageBox { - void operator()(std::string message) const + void operator()(std::string filePath) const { + if (filePath.empty()) + { + MWBase::Environment::get().getWindowManager()->scheduleMessageBox( + "#{OMWEngine:ScreenshotFailed}", MWGui::ShowInDialogueMode_Never); + + return; + } + + std::string messageFormat + = MWBase::Environment::get().getL10nManager()->getMessage("OMWEngine", "ScreenshotMade"); + + std::string message = Misc::StringUtils::format(messageFormat, filePath); + MWBase::Environment::get().getWindowManager()->scheduleMessageBox( std::move(message), MWGui::ShowInDialogueMode_Never); } @@ -149,6 +165,15 @@ namespace private: int mMaxTextureImageUnits = 0; }; + + void reportStats(unsigned frameNumber, osgViewer::Viewer& viewer, std::ostream& stream) + { + viewer.getViewerStats()->report(stream, frameNumber); + osgViewer::Viewer::Cameras cameras; + viewer.getCameras(cameras); + for (osg::Camera* camera : cameras) + camera->getStats()->report(stream, frameNumber); + } } void OMW::Engine::executeLocalScripts() @@ -164,10 +189,9 @@ void OMW::Engine::executeLocalScripts() } } -bool OMW::Engine::frame(float frametime) +bool OMW::Engine::frame(unsigned frameNumber, float frametime) { const osg::Timer_t frameStart = mViewer->getStartTick(); - const unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); const osg::Timer* const timer = osg::Timer::instance(); osg::Stats* const stats = mViewer->getViewerStats(); @@ -221,7 +245,7 @@ bool OMW::Engine::frame(float frametime) if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - if (!mWindowManager->containsMode(MWGui::GM_MainMenu)) + if (!mWindowManager->containsMode(MWGui::GM_MainMenu) || !paused) { if (mWorld->getScriptsEnabled()) { @@ -313,19 +337,23 @@ bool OMW::Engine::frame(float frametime) mLuaManager->reportStats(frameNumber, *stats); } + mStereoManager->updateSettings(Settings::camera().mNearClip, Settings::camera().mViewingDistance); + mViewer->eventTraversal(); mViewer->updateTraversal(); + // update GUI by world data { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); mWorld->updateWindowManager(); } - mLuaWorker->allowUpdate(); // if there is a separate Lua thread, it starts the update now + // if there is a separate Lua thread, it starts the update now + mLuaWorker->allowUpdate(frameStart, frameNumber, *stats); mViewer->renderingTraversals(); - mLuaWorker->finishUpdate(); + mLuaWorker->finishUpdate(frameStart, frameNumber, *stats); return true; } @@ -346,7 +374,6 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mActivationDistanceOverride(-1) , mGrab(true) , mRandomSeed(0) - , mScriptBlacklistUse(true) , mNewGame(false) , mCfgMgr(configurationManager) , mGlMaxTextureImageUnits(0) @@ -423,6 +450,9 @@ void OMW::Engine::addArchive(const std::string& archive) void OMW::Engine::setResourceDir(const std::filesystem::path& parResDir) { mResDir = parResDir; + if (!Version::checkResourcesVersion(mResDir)) + Log(Debug::Error) << "Resources dir " << mResDir + << " doesn't match OpenMW binary, the game may work incorrectly."; } // Set start cell name @@ -449,14 +479,13 @@ void OMW::Engine::setSkipMenu(bool skipMenu, bool newGame) void OMW::Engine::createWindow() { - int screen = Settings::Manager::getInt("screen", "Video"); - int width = Settings::Manager::getInt("resolution x", "Video"); - int height = Settings::Manager::getInt("resolution y", "Video"); - Settings::WindowMode windowMode - = static_cast(Settings::Manager::getInt("window mode", "Video")); - bool windowBorder = Settings::Manager::getBool("window border", "Video"); - int vsync = Settings::Manager::getInt("vsync mode", "Video"); - unsigned int antialiasing = std::max(0, Settings::Manager::getInt("antialiasing", "Video")); + const int screen = Settings::video().mScreen; + const int width = Settings::video().mResolutionX; + const int height = Settings::video().mResolutionY; + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + const bool windowBorder = Settings::video().mWindowBorder; + const SDLUtil::VSyncMode vsync = Settings::video().mVsyncMode; + unsigned antialiasing = static_cast(Settings::video().mAntialiasing); int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); @@ -479,8 +508,7 @@ void OMW::Engine::createWindow() if (!windowBorder) flags |= SDL_WINDOW_BORDERLESS; - SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, - Settings::Manager::getBool("minimize on focus loss", "Video") ? "1" : "0"); + SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, Settings::video().mMinimizeOnFocusLoss ? "1" : "0"); checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); @@ -510,7 +538,7 @@ void OMW::Engine::createWindow() Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing / 2; antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); + Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } @@ -557,7 +585,7 @@ void OMW::Engine::createWindow() SDL_DestroyWindow(mWindow); mWindow = nullptr; antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); + Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } @@ -582,6 +610,7 @@ void OMW::Engine::createWindow() mViewer->setRealizeOperation(realizeOperations); osg::ref_ptr identifyOp = new IdentifyOpenGLOperation(); realizeOperations->add(identifyOp); + realizeOperations->add(new SceneUtil::GetGLExtensionsOperation()); if (Debug::shouldDebugOpenGL()) realizeOperations->add(new Debug::EnableGLDebugOperation()); @@ -590,7 +619,63 @@ void OMW::Engine::createWindow() realizeOperations->add(mSelectColorFormatOperation); if (Stereo::getStereo()) - realizeOperations->add(new Stereo::InitializeStereoOperation()); + { + Stereo::Settings settings; + + settings.mMultiview = Settings::stereo().mMultiview; + settings.mAllowDisplayListsForMultiview = Settings::stereo().mAllowDisplayListsForMultiview; + settings.mSharedShadowMaps = Settings::stereo().mSharedShadowMaps; + + if (Settings::stereo().mUseCustomView) + { + const osg::Vec3 leftEyeOffset(Settings::stereoView().mLeftEyeOffsetX, + Settings::stereoView().mLeftEyeOffsetY, Settings::stereoView().mLeftEyeOffsetZ); + + const osg::Quat leftEyeOrientation(Settings::stereoView().mLeftEyeOrientationX, + Settings::stereoView().mLeftEyeOrientationY, Settings::stereoView().mLeftEyeOrientationZ, + Settings::stereoView().mLeftEyeOrientationW); + + const osg::Vec3 rightEyeOffset(Settings::stereoView().mRightEyeOffsetX, + Settings::stereoView().mRightEyeOffsetY, Settings::stereoView().mRightEyeOffsetZ); + + const osg::Quat rightEyeOrientation(Settings::stereoView().mRightEyeOrientationX, + Settings::stereoView().mRightEyeOrientationY, Settings::stereoView().mRightEyeOrientationZ, + Settings::stereoView().mRightEyeOrientationW); + + settings.mCustomView = Stereo::CustomView{ + .mLeft = Stereo::View{ + .pose = Stereo::Pose{ + .position = leftEyeOffset, + .orientation = leftEyeOrientation, + }, + .fov = Stereo::FieldOfView{ + .angleLeft = Settings::stereoView().mLeftEyeFovLeft, + .angleRight = Settings::stereoView().mLeftEyeFovRight, + .angleUp = Settings::stereoView().mLeftEyeFovUp, + .angleDown = Settings::stereoView().mLeftEyeFovDown, + }, + }, + .mRight = Stereo::View{ + .pose = Stereo::Pose{ + .position = rightEyeOffset, + .orientation = rightEyeOrientation, + }, + .fov = Stereo::FieldOfView{ + .angleLeft = Settings::stereoView().mRightEyeFovLeft, + .angleRight = Settings::stereoView().mRightEyeFovRight, + .angleUp = Settings::stereoView().mRightEyeFovUp, + .angleDown = Settings::stereoView().mRightEyeFovDown, + }, + }, + }; + } + + if (Settings::stereo().mUseCustomEyeResolution) + settings.mEyeResolution + = osg::Vec2i(Settings::stereoView().mEyeResolutionX, Settings::stereoView().mEyeResolutionY); + + realizeOperations->add(new Stereo::InitializeStereoOperation(settings)); + } mViewer->realize(); mGlMaxTextureImageUnits = identifyOp->getMaxTextureImageUnits(); @@ -629,9 +714,9 @@ void OMW::Engine::prepareEngine() mStateManager = std::make_unique(mCfgMgr.getUserDataPath() / "saves", mContentFiles); mEnvironment.setStateManager(*mStateManager); - bool stereoEnabled - = Settings::Manager::getBool("stereo enabled", "Stereo") || osg::DisplaySettings::instance().get()->getStereo(); - mStereoManager = std::make_unique(mViewer, stereoEnabled); + const bool stereoEnabled = Settings::stereo().mStereoEnabled || osg::DisplaySettings::instance().get()->getStereo(); + mStereoManager = std::make_unique( + mViewer, stereoEnabled, Settings::camera().mNearClip, Settings::camera().mViewingDistance); osg::ref_ptr rootNode(new osg::Group); mViewer->setSceneData(rootNode); @@ -642,7 +727,8 @@ void OMW::Engine::prepareEngine() VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); - mResourceSystem = std::make_unique(mVFS.get()); + mResourceSystem = std::make_unique( + mVFS.get(), Settings::cells().mCacheExpiryDelay, &mEncoder.get()->getStatelessEncoder()); mResourceSystem->getSceneManager()->getShaderManager().setMaxTextureUnits(mGlMaxTextureImageUnits); mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply( false); // keep to Off for now to allow better state sharing @@ -656,9 +742,8 @@ void OMW::Engine::prepareEngine() mScreenCaptureOperation = new SceneUtil::AsyncScreenCaptureOperation(mWorkQueue, new SceneUtil::WriteScreenshotToFileOperation(mCfgMgr.getScreenshotPath(), Settings::general().mScreenshotFormat, - Settings::general().mNotifyOnSavedScreenshot - ? std::function(ScheduleNonDialogMessageBox{}) - : std::function(IgnoreString{}))); + Settings::general().mNotifyOnSavedScreenshot ? std::function(ScreenCaptureMessageBox{}) + : std::function(IgnoreString{}))); mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); @@ -706,13 +791,13 @@ void OMW::Engine::prepareEngine() // gui needs our shaders path before everything else mResourceSystem->getSceneManager()->setShaderPath(mResDir / "shaders"); - osg::ref_ptr exts = osg::GLExtensions::Get(0, false); - bool shadersSupported = exts && (exts->glslLanguageVersion >= 1.2f); + osg::GLExtensions& exts = SceneUtil::getGLExtensions(); + bool shadersSupported = exts.glslLanguageVersion >= 1.2f; #if OSG_VERSION_LESS_THAN(3, 6, 6) // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028 - if (exts) - exts->glRenderbufferStorageMultisampleCoverageNV = nullptr; + if (!osg::isGLExtensionSupported(exts.contextID, "NV_framebuffer_multisample_coverage")) + exts.glRenderbufferStorageMultisampleCoverageNV = nullptr; #endif osg::ref_ptr guiRoot = new osg::Group; @@ -723,11 +808,11 @@ void OMW::Engine::prepareEngine() mWindowManager = std::make_unique(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, - Version::getOpenmwVersionDescription(mResDir), shadersSupported, mCfgMgr); + Version::getOpenmwVersionDescription(), shadersSupported, mCfgMgr); mEnvironment.setWindowManager(*mWindowManager); - mInputManager = std::make_unique(mWindow, mViewer, mScreenCaptureHandler, - mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); + mInputManager = std::make_unique(mWindow, mViewer, mScreenCaptureHandler, keybinderUser, + keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); mEnvironment.setInputManager(*mInputManager); // Create sound system @@ -737,6 +822,9 @@ void OMW::Engine::prepareEngine() // Create the world mWorld = std::make_unique( mResourceSystem.get(), mActivationDistanceOverride, mCellName, mCfgMgr.getUserDataPath()); + mEnvironment.setWorld(*mWorld); + mEnvironment.setWorldModel(mWorld->getWorldModel()); + mEnvironment.setESMStore(mWorld->getStore()); Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::AsyncListener asyncListener(*listener); @@ -759,13 +847,10 @@ void OMW::Engine::prepareEngine() } listener->loadingOff(); - mWorld->init(mViewer, rootNode, mWorkQueue.get(), *mUnrefQueue); + mWorld->init(mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue); + mEnvironment.setWorldScene(mWorld->getWorldScene()); mWorld->setupPlayer(); mWorld->setRandomSeed(mRandomSeed); - mEnvironment.setWorld(*mWorld); - mEnvironment.setWorldModel(mWorld->getWorldModel()); - mEnvironment.setWorldScene(mWorld->getWorldScene()); - mEnvironment.setESMStore(mWorld->getStore()); const MWWorld::Store* gmst = &mWorld->getStore().get(); mL10nManager->setGmstLoader( @@ -798,8 +883,7 @@ void OMW::Engine::prepareEngine() mScriptContext = std::make_unique(MWScript::CompilerContext::Type_Full); mScriptContext->setExtensions(&mExtensions); - mScriptManager = std::make_unique(mWorld->getStore(), *mScriptContext, mWarningsMode, - mScriptBlacklistUse ? mScriptBlacklist : std::vector()); + mScriptManager = std::make_unique(mWorld->getStore(), *mScriptContext, mWarningsMode); mEnvironment.setScriptManager(*mScriptManager); // Create game mechanics system @@ -829,11 +913,11 @@ void OMW::Engine::prepareEngine() << 100 * static_cast(result.second) / result.first << "%)"; } - mLuaManager->init(); mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath()); + mLuaManager->init(); // starts a separate lua thread if "lua num threads" > 0 - mLuaWorker = std::make_unique(*mLuaManager, *mViewer); + mLuaWorker = std::make_unique(*mLuaManager); } // Initialise and enter main loop. @@ -863,7 +947,7 @@ void OMW::Engine::go() // Do not try to outsmart the OS thread scheduler (see bug #4785). mViewer->setUseConfigureAffinity(false); - mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); + mEnvironment.setFrameRateLimit(Settings::video().mFramerateLimit); prepareEngine(); @@ -889,17 +973,17 @@ void OMW::Engine::go() } // Setup profiler - osg::ref_ptr statshandler = new Resource::Profiler(stats.is_open(), mVFS.get()); + osg::ref_ptr statsHandler = new Resource::Profiler(stats.is_open(), *mVFS); - initStatsHandler(*statshandler); + initStatsHandler(*statsHandler); - mViewer->addEventHandler(statshandler); + mViewer->addEventHandler(statsHandler); - osg::ref_ptr resourceshandler = new Resource::StatsHandler(stats.is_open(), mVFS.get()); - mViewer->addEventHandler(resourceshandler); + osg::ref_ptr resourcesHandler = new Resource::StatsHandler(stats.is_open(), *mVFS); + mViewer->addEventHandler(resourcesHandler); if (stats.is_open()) - Resource::CollectStatistics(mViewer); + Resource::collectStatistics(*mViewer); // Start the game if (!mSaveGameFile.empty()) @@ -910,7 +994,12 @@ void OMW::Engine::go() { // start in main menu mWindowManager->pushGuiMode(MWGui::GM_MainMenu); - mSoundManager->playPlaylist("Title"); + + if (mVFS->exists(MWSound::titleMusic)) + mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal); + else + Log(Debug::Warning) << "Title music not found"; + std::string_view logo = Fallback::Map::getString("Movies_Morrowind_Logo"); if (!logo.empty()) mWindowManager->playVideo(logo, /*allowSkipping*/ true, /*overrideSounds*/ false); @@ -936,29 +1025,34 @@ void OMW::Engine::go() .count() * timeManager.getSimulationTimeScale(); - mViewer->advance(timeManager.getSimulationTime()); + mViewer->advance(timeManager.getRenderingSimulationTime()); - if (!frame(dt)) + const unsigned frameNumber = mViewer->getFrameStamp()->getFrameNumber(); + + if (!frame(frameNumber, dt)) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } timeManager.updateIsPaused(); if (!timeManager.isPaused()) + { timeManager.setSimulationTime(timeManager.getSimulationTime() + dt); + timeManager.setRenderingSimulationTime(timeManager.getRenderingSimulationTime() + dt); + } if (stats) { + // The delay is required because rendering happens in parallel to the main thread and stats from there is + // available with delay. constexpr unsigned statsReportDelay = 3; - const auto frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (frameNumber >= statsReportDelay) { - const unsigned reportFrameNumber = frameNumber - statsReportDelay; - mViewer->getViewerStats()->report(stats, reportFrameNumber); - osgViewer::Viewer::Cameras cameras; - mViewer->getCameras(cameras); - for (auto camera : cameras) - camera->getStats()->report(stats, reportFrameNumber); + // Viewer frame number can be different from frameNumber because of loading screens which render new + // frames inside a simulation frame. + const unsigned currentFrameNumber = mViewer->getFrameStamp()->getFrameNumber(); + for (unsigned i = frameNumber; i <= currentFrameNumber; ++i) + reportStats(i - statsReportDelay, *mViewer, stats); } } @@ -1015,16 +1109,6 @@ void OMW::Engine::setWarningsMode(int mode) mWarningsMode = mode; } -void OMW::Engine::setScriptBlacklist(const std::vector& list) -{ - mScriptBlacklist = list; -} - -void OMW::Engine::setScriptBlacklistUse(bool use) -{ - mScriptBlacklistUse = use; -} - void OMW::Engine::setSaveGameFile(const std::filesystem::path& savegame) { mSaveGameFile = savegame; diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 2cd224785b..39056af560 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -178,8 +178,6 @@ namespace OMW Files::Collections mFileCollections; Translation::Storage mTranslationDataStorage; - std::vector mScriptBlacklist; - bool mScriptBlacklistUse; bool mNewGame; // not implemented @@ -188,7 +186,7 @@ namespace OMW void executeLocalScripts(); - bool frame(float dt); + bool frame(unsigned frameNumber, float dt); /// Prepare engine for game play void prepareEngine(); @@ -253,10 +251,6 @@ namespace OMW void setWarningsMode(int mode); - void setScriptBlacklist(const std::vector& list); - - void setScriptBlacklistUse(bool use); - /// Set the save game file to load after initialising the engine. void setSaveGameFile(const std::filesystem::path& savegame); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index e326e0aa2b..5b89bca960 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -14,7 +15,7 @@ #include #if defined(_WIN32) -#include +#include // makes __argc and __argv available on windows #include @@ -52,33 +53,20 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati if (variables.count("help")) { - getRawStdout() << desc << std::endl; + Debug::getRawStdout() << desc << std::endl; return false; } if (variables.count("version")) { - cfgMgr.readConfiguration(variables, desc, true); - - Version::Version v - = Version::getOpenmwVersion(variables["resources"] - .as() - .u8string()); // This call to u8string is redundant, but required to build - // on MSVC 14.26 due to implementation bugs. - getRawStdout() << v.describe() << std::endl; + Debug::getRawStdout() << Version::getOpenmwVersionDescription() << std::endl; return false; } cfgMgr.readConfiguration(variables, desc); - setupLogging(cfgMgr.getLogPath(), "OpenMW"); - - Version::Version v - = Version::getOpenmwVersion(variables["resources"] - .as() - .u8string()); // This call to u8string is redundant, but required to build on - // MSVC 14.26 due to implementation bugs. - Log(Debug::Info) << v.describe(); + Debug::setupLogging(cfgMgr.getLogPath(), "OpenMW"); + Log(Debug::Info) << Version::getOpenmwVersionDescription(); Settings::Manager::load(cfgMgr); @@ -121,7 +109,8 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting..."; return false; } - std::set contentDedupe; + engine.addContentFile("builtin.omwscripts"); + std::set contentDedupe{ "builtin.omwscripts" }; for (const auto& contentFile : content) { if (!contentDedupe.insert(contentFile).second) @@ -160,14 +149,6 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati engine.setScriptConsoleMode(variables["script-console"].as()); engine.setStartupScript(variables["script-run"].as()); engine.setWarningsMode(variables["script-warn"].as()); - std::vector scriptBlacklist; - auto& scriptBlacklistString = variables["script-blacklist"].as(); - for (const auto& blacklistString : scriptBlacklistString) - { - scriptBlacklist.push_back(ESM::RefId::stringRefId(blacklistString)); - } - engine.setScriptBlacklist(scriptBlacklist); - engine.setScriptBlacklistUse(variables["script-blacklist-use"].as()); engine.setSaveGameFile(variables["load-savegame"].as().u8string()); // other settings @@ -232,8 +213,6 @@ int runApplication(int argc, char* argv[]) Platform::init(); #ifdef __APPLE__ - std::filesystem::path binary_path = std::filesystem::absolute(std::filesystem::path(argv[0])); - std::filesystem::current_path(binary_path.parent_path()); setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif @@ -243,6 +222,9 @@ int runApplication(int argc, char* argv[]) if (parseOptions(argc, argv, *engine, cfgMgr)) { + if (!Misc::checkRequiredOSGPluginsArePresent()) + return 1; + engine->go(); } @@ -255,7 +237,7 @@ extern "C" int SDL_main(int argc, char** argv) int main(int argc, char** argv) #endif { - return wrapApplication(&runApplication, argc, argv, "OpenMW"); + return Debug::wrapApplication(&runApplication, argc, argv, "OpenMW"); } // Platform specific for Windows when there is no console built into the executable. diff --git a/apps/openmw/mwbase/dialoguemanager.hpp b/apps/openmw/mwbase/dialoguemanager.hpp index 70887c69c1..e4c60e596d 100644 --- a/apps/openmw/mwbase/dialoguemanager.hpp +++ b/apps/openmw/mwbase/dialoguemanager.hpp @@ -2,6 +2,7 @@ #define GAME_MWBASE_DIALOGUEMANAGER_H #include +#include #include #include #include @@ -65,7 +66,7 @@ namespace MWBase virtual void goodbye() = 0; - virtual void say(const MWWorld::Ptr& actor, const ESM::RefId& topic) = 0; + virtual bool say(const MWWorld::Ptr& actor, const ESM::RefId& topic) = 0; virtual void keywordSelected(std::string_view keyword, ResponseCallback* callback) = 0; virtual void goodbyeSelected() = 0; @@ -108,11 +109,15 @@ namespace MWBase /// Changes faction1's opinion of faction2 by \a diff. virtual void modFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int diff) = 0; + /// Set faction1's opinion of faction2. virtual void setFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int absolute) = 0; /// @return faction1's opinion of faction2 virtual int getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2) const = 0; + /// @return all faction's opinion overrides + virtual const std::map* getFactionReactionOverrides(const ESM::RefId& faction) const = 0; + /// Removes the last added topic response for the given actor from the journal virtual void clearInfoActor(const MWWorld::Ptr& actor) const = 0; }; diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index f52f9ea454..5ee20476b3 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -45,6 +45,7 @@ namespace MWBase virtual void processChangedSettings(const std::set>& changed) = 0; virtual void setDragDrop(bool dragDrop) = 0; + virtual bool isGamepadGuiCursorEnabled() = 0; virtual void setGamepadGuiCursorEnabled(bool enabled) = 0; virtual void toggleControlSwitch(std::string_view sw, bool value) = 0; diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 12a894d343..a5d6fe1114 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -8,6 +8,7 @@ #include #include "../mwgui/mode.hpp" +#include "../mwrender/animationpriority.hpp" #include namespace MWWorld @@ -29,6 +30,14 @@ namespace ESM struct LuaScripts; } +namespace LuaUtil +{ + namespace InputAction + { + class Registry; + } +} + namespace MWBase { // \brief LuaManager is the central interface through which the engine invokes lua scripts. @@ -46,23 +55,40 @@ namespace MWBase virtual void newGameStarted() = 0; virtual void gameLoaded() = 0; + virtual void gameEnded() = 0; + virtual void noGame() = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0; virtual void objectTeleported(const MWWorld::Ptr& ptr) = 0; virtual void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) = 0; virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; + virtual void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) = 0; + virtual void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) = 0; + virtual void playAnimation(const MWWorld::Ptr& object, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback) + = 0; + virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0; + virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; + virtual void actorDied(const MWWorld::Ptr& actor) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; - // `arg` is either forwarded from MWGui::pushGuiMode or empty virtual void uiModeChanged(const MWWorld::Ptr& arg) = 0; // TODO: notify LuaManager about other events // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, - // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) = 0; + // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, + // DamageSourceType sourceType) = 0; struct InputEvent { + struct WheelChange + { + int x; + int y; + }; + enum { KeyPressed, @@ -73,8 +99,11 @@ namespace MWBase TouchPressed, TouchReleased, TouchMoved, + MouseButtonPressed, + MouseButtonReleased, + MouseWheel, } mType; - std::variant mValue; + std::variant mValue; }; virtual void inputEvent(const InputEvent& event) = 0; diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 5319ab6dde..4883fa2000 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -9,6 +9,7 @@ #include #include "../mwmechanics/greetingstate.hpp" +#include "../mwrender/animationpriority.hpp" #include "../mwworld/ptr.hpp" @@ -104,7 +105,9 @@ namespace MWBase virtual bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0; /// Makes \a ptr fight \a target. Also shouts a combat taunt. - virtual void startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; + virtual void startCombat( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) + = 0; /// Removes an actor and its allies from combat with the actor's targets. virtual void stopCombat(const MWWorld::Ptr& ptr) = 0; @@ -165,16 +168,33 @@ namespace MWBase ///< Forces an object to refresh its animation state. virtual bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number = 1, bool persist = false) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number = 1, bool scripted = false) = 0; ///< Run animation for a MW-reference. Calls to this function for references that are currently not /// in the scene should be ignored. /// /// \param mode 0 normal, 1 immediate start, 2 immediate loop - /// \param count How many times the animation should be run - /// \param persist Whether the animation state should be stored in saved games - /// and persist after cell unload. + /// \param number How many times the animation should be run + /// \param scripted Whether the animation should be treated as a scripted animation. /// \return Success or error + virtual bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + = 0; + ///< Lua variant of playAnimationGroup. The mode parameter is omitted + /// and forced to 0. modes 1 and 2 can be emulated by doing clearAnimationQueue() and + /// setting the startKey. + /// + /// \param number How many times the animation should be run + /// \param speed How fast to play the animation, where 1.f = normal speed + /// \param startKey Which textkey to start the animation from + /// \param stopKey Which textkey to stop the animation on + /// \param forceLoop Force the animation to be looping, even if it's normally not looping. + /// \param blendMask See MWRender::Animation::BlendMask + /// \param scripted Whether the animation should be treated as as scripted animation + /// \return Success or error + /// + + virtual void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) = 0; virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0; ///< Skip the animation for the given MW-reference for one frame. Calls to this function for @@ -182,9 +202,14 @@ namespace MWBase virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; + virtual bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const = 0; + /// Save the current animation state of managed references to their RefData. virtual void persistAnimationStates() = 0; + /// Clear out the animation queue, and cancel any animation currently playing from the queue + virtual void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) = 0; + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) virtual void updateMagicEffects(const MWWorld::Ptr& ptr) = 0; @@ -235,7 +260,7 @@ namespace MWBase virtual bool isReadyToBlock(const MWWorld::Ptr& ptr) const = 0; virtual bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const = 0; - virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) = 0; + virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) = 0; virtual void processChangedSettings(const std::set>& settings) = 0; diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 3dd9cd3b33..5f96a4e095 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -6,6 +6,8 @@ #include #include +#include + #include "../mwsound/type.hpp" #include "../mwworld/ptr.hpp" @@ -29,6 +31,12 @@ namespace MWSound MaxCount }; + enum class MusicType + { + Normal, + MWScript + }; + class Sound; class Stream; struct Sound_Decoder; @@ -101,26 +109,28 @@ namespace MWBase virtual void processChangedSettings(const std::set>& settings) = 0; + virtual bool isEnabled() const = 0; + ///< Returns true if sound system is enabled + virtual void stopMusic() = 0; ///< Stops music if it's playing - virtual void streamMusic(const std::string& filename) = 0; + virtual MWSound::MusicType getMusicType() const = 0; + + virtual void streamMusic(VFS::Path::NormalizedView filename, MWSound::MusicType type, float fade = 1.f) = 0; ///< Play a soundifle - /// \param filename name of a sound file in "Music/" in the data directory. + /// \param filename name of a sound file in the data directory. + /// \param type music type. + /// \param fade time in seconds to fade out current track before start this one. virtual bool isMusicPlaying() = 0; ///< Returns true if music is playing - virtual void playPlaylist(const std::string& playlist) = 0; - ///< Start playing music from the selected folder - /// \param name of the folder that contains the playlist - /// Title music playlist is predefined - - virtual void say(const MWWorld::ConstPtr& reference, const std::string& filename) = 0; + virtual void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) = 0; ///< Make an actor say some text. /// \param filename name of a sound file in the VFS - virtual void say(const std::string& filename) = 0; + virtual void say(VFS::Path::NormalizedView filename) = 0; ///< Say some text, without an actor ref /// \param filename name of a sound file in the VFS diff --git a/apps/openmw/mwbase/statemanager.hpp b/apps/openmw/mwbase/statemanager.hpp index 1a25da32b0..35435e1430 100644 --- a/apps/openmw/mwbase/statemanager.hpp +++ b/apps/openmw/mwbase/statemanager.hpp @@ -44,6 +44,9 @@ namespace MWBase virtual void askLoadRecent() = 0; + virtual void requestNewGame() = 0; + virtual void requestLoad(const std::filesystem::path& filepath) = 0; + virtual State getState() const = 0; virtual void newGame(bool bypass = false) = 0; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 727aee4abc..0b2e85f3e1 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -77,6 +77,7 @@ namespace MWGui class JailScreen; class MessageBox; class PostProcessorHud; + class SettingsWindow; enum ShowInDialogueMode { @@ -119,7 +120,7 @@ namespace MWBase virtual void pushGuiMode(MWGui::GuiMode mode, const MWWorld::Ptr& arg) = 0; virtual void pushGuiMode(MWGui::GuiMode mode) = 0; - virtual void popGuiMode() = 0; + virtual void popGuiMode(bool forceExit = false) = 0; virtual void removeGuiMode(MWGui::GuiMode mode) = 0; ///< can be anywhere in the stack @@ -134,8 +135,9 @@ namespace MWBase virtual bool isGuiMode() const = 0; virtual bool isConsoleMode() const = 0; - virtual bool isPostProcessorHudVisible() const = 0; + virtual bool isSettingsWindowVisible() const = 0; + virtual bool isInteractiveMessageBoxActive() const = 0; virtual void toggleVisible(MWGui::GuiWindow wnd) = 0; @@ -164,7 +166,8 @@ namespace MWBase virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; virtual MWWorld::Ptr getConsoleSelectedObject() const = 0; - virtual void setConsoleMode(const std::string& mode) = 0; + virtual void setConsoleMode(std::string_view mode) = 0; + virtual const std::string& getConsoleMode() = 0; static constexpr std::string_view sConsoleColor_Default = "#FFFFFF"; static constexpr std::string_view sConsoleColor_Error = "#FF2222"; @@ -199,9 +202,6 @@ namespace MWBase virtual bool getFullHelp() const = 0; - virtual void setActiveMap(int x, int y, bool interior) = 0; - ///< set the indices of the map texture that should be used - /// sets the visibility of the drowning bar virtual void setDrowningBarVisibility(bool visible) = 0; @@ -229,7 +229,8 @@ namespace MWBase virtual void unsetSelectedWeapon() = 0; virtual void showCrosshair(bool show) = 0; - virtual bool toggleHud() = 0; + virtual bool setHudVisibility(bool show) = 0; + virtual bool isHudVisible() const = 0; virtual void disallowMouse() = 0; virtual void allowMouse() = 0; @@ -253,8 +254,8 @@ namespace MWBase = 0; virtual void staticMessageBox(std::string_view message) = 0; virtual void removeStaticMessageBox() = 0; - virtual void interactiveMessageBox( - std::string_view message, const std::vector& buttons = {}, bool block = false) + virtual void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) = 0; /// returns the index of the pressed button or -1 if no button was pressed @@ -289,7 +290,7 @@ namespace MWBase virtual void setEnemy(const MWWorld::Ptr& enemy) = 0; - virtual int getMessagesCount() const = 0; + virtual std::size_t getMessagesCount() const = 0; virtual const Translation::Storage& getTranslationDataStorage() const = 0; @@ -341,6 +342,7 @@ namespace MWBase virtual void toggleConsole() = 0; virtual void toggleDebugWindow() = 0; virtual void togglePostProcessorHud() = 0; + virtual void toggleSettingsWindow() = 0; /// Cycle to next or previous spell virtual void cycleSpell(bool next) = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 40d57cbb6e..904b96c463 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -4,13 +4,13 @@ #include "rotationflags.hpp" #include -#include #include #include #include #include #include +#include #include "../mwworld/doorstate.hpp" #include "../mwworld/globalvariablename.hpp" @@ -148,7 +148,7 @@ namespace MWBase virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; virtual MWWorld::ESMStore& getStore() = 0; - const MWWorld::ESMStore& getStore() const { return const_cast(this)->getStore(); } + virtual const MWWorld::ESMStore& getStore() const = 0; virtual const std::vector& getESMVersions() const = 0; @@ -183,9 +183,7 @@ namespace MWBase /// generate a name. virtual std::string_view getCellName(const MWWorld::Cell& cell) const = 0; - virtual std::string_view getCellName(const ESM::Cell* cell) const = 0; - - virtual void removeRefScript(MWWorld::RefData* ref) = 0; + virtual void removeRefScript(const MWWorld::CellRef* ref) = 0; //< Remove the script attached to ref from mLocalScripts virtual MWWorld::Ptr getPtr(const ESM::RefId& name, bool activeOnly) = 0; @@ -232,7 +230,7 @@ namespace MWBase virtual void setMoonColour(bool red) = 0; - virtual void modRegion(const ESM::RefId& regionid, const std::vector& chances) = 0; + virtual void modRegion(const ESM::RefId& regionid, const std::vector& chances) = 0; virtual void changeToInteriorCell( std::string_view cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent = true) @@ -252,13 +250,6 @@ namespace MWBase virtual float getMaxActivationDistance() const = 0; - /// Returns a pointer to the object the provided object would hit (if within the - /// specified distance), and the point where the hit occurs. This will attempt to - /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. - virtual std::pair getHitContact( - const MWWorld::ConstPtr& ptr, float distance, std::vector& targets) - = 0; - virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) = 0; ///< Adjust position after load to be on ground. Must be called after model load. /// @param force do this even if the ptr is flying @@ -311,7 +302,7 @@ namespace MWBase virtual const MWPhysics::RayCastingInterface* getRayCasting() const = 0; virtual bool castRenderingRay(MWPhysics::RayCastingResult& res, const osg::Vec3f& from, const osg::Vec3f& to, - bool ignorePlayer, bool ignoreActors) + bool ignorePlayer, bool ignoreActors, std::span ignoreList = {}) = 0; virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; @@ -434,7 +425,6 @@ namespace MWBase /// \todo this does not belong here virtual void screenshot(osg::Image* image, int w, int h) = 0; - virtual bool screenshot360(osg::Image* image) = 0; /// Find default position inside exterior cell specified by name /// \return empty RefId if exterior with given name not exists, the cell's RefId otherwise @@ -470,7 +460,7 @@ namespace MWBase */ virtual MWWorld::SpellCastState startSpellCast(const MWWorld::Ptr& actor) = 0; - virtual void castSpell(const MWWorld::Ptr& actor, bool manualSpell = false) = 0; + virtual void castSpell(const MWWorld::Ptr& actor, bool scriptedSpell = false) = 0; virtual void launchMagicBolt(const ESM::RefId& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, ESM::RefNum item) @@ -525,7 +515,7 @@ namespace MWBase /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; - virtual void spawnEffect(const std::string& model, const std::string& textureOverride, + virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) = 0; @@ -546,9 +536,6 @@ namespace MWBase const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) = 0; - /// Return the distance between actor's weapon and target's collision box. - virtual float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0; - virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index 9e99b4cacb..6de211e557 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -1,6 +1,7 @@ #include "activator.hpp" #include +#include #include #include @@ -27,7 +28,6 @@ #include "../mwrender/vismask.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwmechanics/npcstats.hpp" @@ -59,10 +59,10 @@ namespace MWClass void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } - std::string Activator::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Activator::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } @@ -102,16 +102,13 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); - std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; return info; } @@ -142,16 +139,14 @@ namespace MWClass ESM::RefId Activator::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { - const std::string model - = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise + // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise + const std::string_view model = getModel(ptr); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::RefId* creatureId = nullptr; - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); for (const ESM::Creature& iter : store.get()) { - if (!iter.mModel.empty() - && Misc::StringUtils::ciEqual(model, Misc::ResourceHelpers::correctMeshPath(iter.mModel, vfs))) + if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, iter.mModel)) { creatureId = !iter.mOriginal.empty() ? &iter.mOriginal : &iter.mId; break; diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 309e163abe..ec0f1bf282 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -41,7 +41,7 @@ namespace MWClass std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool useAnim() const override; ///< Whether or not to use animated variant of model (default false) diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 152a4bbba9..ea0b0d07b3 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -3,6 +3,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" @@ -14,6 +15,7 @@ #include "../mwphysics/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/worldmodel.hpp" namespace MWClass { @@ -25,7 +27,7 @@ namespace MWClass void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - physics.addActor(ptr, model); + physics.addActor(ptr, VFS::Path::toNormalized(model)); if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } @@ -35,23 +37,6 @@ namespace MWClass return true; } - void Actor::block(const MWWorld::Ptr& ptr) const - { - const MWWorld::InventoryStore& inv = getInventoryStore(ptr); - MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end()) - return; - - MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - const ESM::RefId skill = shield->getClass().getEquipmentSkill(*shield); - if (skill == ESM::Skill::LightArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); - else if (skill == ESM::Skill::MediumArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); - else if (skill == ESM::Skill::HeavyArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); - } - osg::Vec3f Actor::getRotationVector(const MWWorld::Ptr& ptr) const { MWMechanics::Movement& movement = getMovementSettings(ptr); @@ -90,4 +75,19 @@ namespace MWClass moveSpeed *= 0.75f; return moveSpeed; } + + bool Actor::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const + { + MWMechanics::CastSpell cast(actor, actor); + const ESM::RefId& recordId = consumable.getCellRef().getRefId(); + MWBase::Environment::get().getWorldModel()->registerPtr(consumable); + MWBase::Environment::get().getLuaManager()->itemConsumed(consumable, actor); + actor.getClass().getContainerStore(actor).remove(consumable, 1); + if (cast.cast(recordId)) + { + MWBase::Environment::get().getWorld()->breakInvisibility(actor); + return true; + } + return false; + } } diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index 5a143c7a7d..cf0cb1eaa5 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -45,8 +45,6 @@ namespace MWClass bool useAnim() const override; - void block(const MWWorld::Ptr& ptr) const override; - osg::Vec3f getRotationVector(const MWWorld::Ptr& ptr) const override; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. @@ -62,6 +60,8 @@ namespace MWClass /// Return current movement speed. float getCurrentSpeed(const MWWorld::Ptr& ptr) const override; + bool consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const override; + // not implemented Actor(const Actor&) = delete; Actor& operator=(const Actor&) = delete; diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index 2fbe2f9f87..90112648b5 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -1,6 +1,7 @@ #include "apparatus.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -15,9 +16,9 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -35,17 +36,14 @@ namespace MWClass } } - std::string Apparatus::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Apparatus::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Apparatus::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Apparatus::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -92,8 +90,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -103,10 +100,10 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/apparatus.hpp b/apps/openmw/mwclass/apparatus.hpp index ce0916c079..c6bd45858a 100644 --- a/apps/openmw/mwclass/apparatus.hpp +++ b/apps/openmw/mwclass/apparatus.hpp @@ -49,7 +49,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 54561e3b0f..8bf9071f0c 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -1,6 +1,7 @@ #include "armor.hpp" #include +#include #include #include @@ -24,9 +25,9 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -44,17 +45,14 @@ namespace MWClass } } - std::string Armor::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Armor::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Armor::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Armor::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -217,8 +215,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -258,15 +255,15 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index d464360623..808bc078f4 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -74,7 +74,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/bodypart.cpp b/apps/openmw/mwclass/bodypart.cpp index 431fb69652..81e42ac725 100644 --- a/apps/openmw/mwclass/bodypart.cpp +++ b/apps/openmw/mwclass/bodypart.cpp @@ -42,7 +42,7 @@ namespace MWClass return false; } - std::string BodyPart::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view BodyPart::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } diff --git a/apps/openmw/mwclass/bodypart.hpp b/apps/openmw/mwclass/bodypart.hpp index 4a2d9d3620..4268c1ecf5 100644 --- a/apps/openmw/mwclass/bodypart.hpp +++ b/apps/openmw/mwclass/bodypart.hpp @@ -25,7 +25,7 @@ namespace MWClass bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index b2b65e01b2..9cbaaa9071 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -1,6 +1,7 @@ #include "book.hpp" #include +#include #include #include @@ -19,11 +20,11 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwmechanics/npcstats.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -41,17 +42,14 @@ namespace MWClass } } - std::string Book::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Book::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Book::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Book::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -111,8 +109,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -122,13 +119,13 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.enchant = ref->mBase->mEnchant; - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/book.hpp b/apps/openmw/mwclass/book.hpp index a30e85c107..ca804a32e6 100644 --- a/apps/openmw/mwclass/book.hpp +++ b/apps/openmw/mwclass/book.hpp @@ -54,7 +54,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/classes.cpp b/apps/openmw/mwclass/classes.cpp index 071389c22c..e7d5cf394b 100644 --- a/apps/openmw/mwclass/classes.cpp +++ b/apps/openmw/mwclass/classes.cpp @@ -9,11 +9,15 @@ #include #include #include +#include #include +#include #include #include #include +#include #include +#include #include #include #include @@ -32,7 +36,6 @@ #include "ingredient.hpp" #include "itemlevlist.hpp" #include "light.hpp" -#include "light4.hpp" #include "lockpick.hpp" #include "misc.hpp" #include "npc.hpp" @@ -43,6 +46,8 @@ #include "weapon.hpp" #include "esm4base.hpp" +#include "esm4npc.hpp" +#include "light4.hpp" namespace MWClass { @@ -71,22 +76,26 @@ namespace MWClass BodyPart::registerSelf(); ESM4Named::registerSelf(); - ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); + ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); + ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Light::registerSelf(); ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Npc::registerSelf(); + ESM4Named::registerSelf(); ESM4Static::registerSelf(); + ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Tree::registerSelf(); ESM4Named::registerSelf(); - ESM4Light::registerSelf(); - ESM4Actor::registerSelf(); - ESM4Actor::registerSelf(); } } diff --git a/apps/openmw/mwclass/classmodel.hpp b/apps/openmw/mwclass/classmodel.hpp index 5d1019ee1d..0f633f37bf 100644 --- a/apps/openmw/mwclass/classmodel.hpp +++ b/apps/openmw/mwclass/classmodel.hpp @@ -1,28 +1,18 @@ #ifndef OPENMW_MWCLASS_CLASSMODEL_H #define OPENMW_MWCLASS_CLASSMODEL_H -#include "../mwbase/environment.hpp" - #include "../mwworld/livecellref.hpp" #include "../mwworld/ptr.hpp" -#include -#include - -#include +#include namespace MWClass { template - std::string getClassModel(const MWWorld::ConstPtr& ptr) + std::string_view getClassModel(const MWWorld::ConstPtr& ptr) { const MWWorld::LiveCellRef* ref = ptr.get(); - - if (!ref->mBase->mModel.empty()) - return Misc::ResourceHelpers::correctMeshPath( - ref->mBase->mModel, MWBase::Environment::get().getResourceSystem()->getVFS()); - - return {}; + return ref->mBase->mModel; } } diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index cbc5cecb70..87d34c56d6 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -1,6 +1,7 @@ #include "clothing.hpp" #include +#include #include #include @@ -16,12 +17,12 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -39,17 +40,14 @@ namespace MWClass } } - std::string Clothing::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Clothing::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Clothing::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Clothing::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -154,8 +152,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -165,15 +162,15 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index a1e8348713..f95559f9c0 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -66,7 +66,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 19132f30ee..8327904ecd 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -1,6 +1,7 @@ #include "container.hpp" #include +#include #include #include @@ -21,9 +22,9 @@ #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/nullaction.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" @@ -33,6 +34,7 @@ #include "../mwmechanics/npcstats.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -68,10 +70,12 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); MWWorld::LiveCellRef* ref = ptr.get(); // store ptr.getRefData().setCustomData(std::make_unique(*ref->mBase, ptr.getCell())); + getContainerStore(ptr).setPtr(ptr); MWBase::Environment::get().getWorld()->addContainerScripts(ptr, ptr.getCell()); } @@ -120,10 +124,10 @@ namespace MWClass void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } - std::string Container::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Container::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } @@ -214,18 +218,13 @@ namespace MWClass std::string_view Container::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } MWWorld::ContainerStore& Container::getContainerStore(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); - auto& data = ptr.getRefData().getCustomData()->asContainerCustomData(); - data.mStore.mPtr = ptr; - return data.mStore; + return ptr.getRefData().getCustomData()->asContainerCustomData().mStore; } ESM::RefId Container::getScript(const MWWorld::ConstPtr& ptr) const @@ -248,7 +247,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; int lockLevel = ptr.getCellRef().getLockLevel(); @@ -264,13 +263,13 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); if (ptr.getCellRef().getRefId() == "stolen_goods") - text += "\nYou can not use evidence chests"; + info.extra += "\nYou cannot use evidence chests"; } - info.text = text; + info.text = std::move(text); return info; } @@ -301,8 +300,13 @@ namespace MWClass MWWorld::Ptr Container::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); - - return MWWorld::Ptr(cell.insert(ref), &cell); + MWWorld::Ptr newPtr(cell.insert(ref), &cell); + if (newPtr.getRefData().getCustomData()) + { + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + getContainerStore(newPtr).setPtr(newPtr); + } + return newPtr; } void Container::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const @@ -312,6 +316,9 @@ namespace MWClass const ESM::ContainerState& containerState = state.asContainerState(); ptr.getRefData().setCustomData(std::make_unique(containerState.mInventory)); + + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + getContainerStore(ptr).setPtr(ptr); } void Container::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 5f8b962e4a..88d8148fdc 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -85,7 +85,7 @@ namespace MWClass void respawn(const MWWorld::Ptr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool useAnim() const override; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 672b198704..007338095f 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -1,6 +1,7 @@ #include "creature.hpp" #include +#include #include #include @@ -39,14 +40,15 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/localscripts.hpp" #include "../mwworld/ptr.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace { @@ -117,6 +119,7 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); auto tempData = std::make_unique(); CreatureCustomData* data = tempData.get(); MWMechanics::CreatureCustomDataResetter resetter{ ptr }; @@ -125,14 +128,8 @@ namespace MWClass MWWorld::LiveCellRef* ref = ptr.get(); // creature stats - data->mCreatureStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mData.mStrength); - data->mCreatureStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mData.mIntelligence); - data->mCreatureStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mData.mWillpower); - data->mCreatureStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mData.mAgility); - data->mCreatureStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mData.mSpeed); - data->mCreatureStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mData.mEndurance); - data->mCreatureStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mData.mPersonality); - data->mCreatureStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mData.mLuck); + for (size_t i = 0; i < ref->mBase->mData.mAttributes.size(); ++i) + data->mCreatureStats.setAttribute(ESM::Attribute::indexToRefId(i), ref->mBase->mData.mAttributes[i]); data->mCreatureStats.setHealth(static_cast(ref->mBase->mData.mHealth)); data->mCreatureStats.setMagicka(static_cast(ref->mBase->mData.mMana)); data->mCreatureStats.setFatigue(static_cast(ref->mBase->mData.mFatigue)); @@ -161,6 +158,7 @@ namespace MWClass data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); + data->mContainerStore->setPtr(ptr); data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); @@ -181,21 +179,22 @@ namespace MWClass objects.insertCreature(ptr, model, hasInventoryStore(ptr)); } - std::string Creature::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Creature::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } - void Creature::getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const + void Creature::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { - std::string model = getModel(ptr); + std::string_view model = getModel(ptr); if (!model.empty()) models.push_back(model); - // FIXME: use const version of InventoryStore functions once they are available - if (hasInventoryStore(ptr)) + const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); + if (customData && hasInventoryStore(ptr)) { - const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); + const auto& invStore + = static_cast(*customData->asCreatureCustomData().mContainerStore); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); @@ -211,10 +210,7 @@ namespace MWClass std::string_view Creature::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } MWMechanics::CreatureStats& Creature::getCreatureStats(const MWWorld::Ptr& ptr) const @@ -245,24 +241,10 @@ namespace MWClass if (!weapon.isEmpty()) dist *= weapon.get()->mBase->mData.mReach; - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit - // result. - std::vector targetActors; - getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors); - - std::pair result - = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors); + const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; - const MWWorld::Class& othercls = result.first.getClass(); - if (!othercls.isActor()) // Can't hit non-actors - return true; - - MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(result.first); - if (otherstats.isDead()) // Can't hit dead actors - return true; - // Note that earlier we returned true in spite of an apparent failure to hit anything alive. // This is because hitting nothing is not a "miss" and should be handled as such character controller-side. victim = result.first; @@ -301,7 +283,8 @@ namespace MWClass if (!success) { - victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); + victim.getClass().onHit( + victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -331,11 +314,11 @@ namespace MWClass { const unsigned char* attack = nullptr; if (type == ESM::Weapon::AT_Chop) - attack = weapon.get()->mBase->mData.mChop; + attack = weapon.get()->mBase->mData.mChop.data(); else if (type == ESM::Weapon::AT_Slash) - attack = weapon.get()->mBase->mData.mSlash; + attack = weapon.get()->mBase->mData.mSlash.data(); else if (type == ESM::Weapon::AT_Thrust) - attack = weapon.get()->mBase->mData.mThrust; + attack = weapon.get()->mBase->mData.mThrust.data(); if (attack) { damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); @@ -355,31 +338,39 @@ namespace MWClass MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) - { damage = 0; - victim.getClass().block(victim); - } MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + victim.getClass().onHit( + victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + // Self defense + bool setOnPcHitMe = true; + // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) + { stats.setAttacked(true); - // Self defense - bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. - - // No retaliation for totally static creatures (they have no movement or attacks anyway) - if (isMobile(ptr) && !attacker.isEmpty()) - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + // No retaliation for totally static creatures (they have no movement or attacks anyway) + if (isMobile(ptr)) + { + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } + } // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) @@ -481,18 +472,17 @@ namespace MWClass } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - if (stats.isDead()) { - // by default user can loot friendly actors during death animation - if (Settings::game().mCanLootDuringDeathAnimation && !stats.getAiSequence().isInCombat()) + // by default user can loot non-fighting actors during death animation + if (Settings::game().mCanLootDuringDeathAnimation) return std::make_unique(ptr); // otherwise wait until death animation if (stats.isDeathAnimationFinished()) return std::make_unique(ptr); } - else if (!stats.getAiSequence().isInCombat() && !stats.getKnockedDown()) + else if (!stats.getKnockedDown()) return std::make_unique(ptr); // Tribunal and some mod companions oddly enough must use open action as fallback @@ -505,10 +495,7 @@ namespace MWClass MWWorld::ContainerStore& Creature::getContainerStore(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); - auto& store = *ptr.getRefData().getCustomData()->asCreatureCustomData().mContainerStore; - if (hasInventoryStore(ptr)) - static_cast(store).setActor(ptr); - return store; + return *ptr.getRefData().getCustomData()->asCreatureCustomData().mContainerStore; } MWWorld::InventoryStore& Creature::getInventoryStore(const MWWorld::Ptr& ptr) const @@ -519,7 +506,7 @@ namespace MWClass throw std::runtime_error("this creature has no inventory store"); } - bool Creature::hasInventoryStore(const MWWorld::Ptr& ptr) const + bool Creature::hasInventoryStore(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Weapon); } @@ -590,7 +577,8 @@ namespace MWClass if (customData.mCreatureStats.isDead() && customData.mCreatureStats.isDeathAnimationFinished()) return true; - return !customData.mCreatureStats.getAiSequence().isInCombat(); + const MWMechanics::AiSequence& aiSeq = customData.mCreatureStats.getAiSequence(); + return !aiSeq.isInCombat() || aiSeq.isFleeing(); } MWGui::ToolTipInfo Creature::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const @@ -599,12 +587,10 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); - std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); - info.text = text; + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); return info; } @@ -658,15 +644,13 @@ namespace MWClass if (sounds.empty()) { - const std::string model = getModel(ptr); + const std::string_view model = getModel(ptr); if (!model.empty()) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); for (const ESM::Creature& creature : store.get()) { if (creature.mId != ourId && creature.mOriginal != ourId && !creature.mModel.empty() - && Misc::StringUtils::ciEqual( - model, Misc::ResourceHelpers::correctMeshPath(creature.mModel, vfs))) + && Misc::StringUtils::ciEqual(model, creature.mModel)) { const ESM::RefId& fallbackId = !creature.mOriginal.empty() ? creature.mOriginal : creature.mId; sound = store.get().begin(); @@ -694,8 +678,13 @@ namespace MWClass MWWorld::Ptr Creature::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); - - return MWWorld::Ptr(cell.insert(ref), &cell); + MWWorld::Ptr newPtr(cell.insert(ref), &cell); + if (newPtr.getRefData().getCustomData()) + { + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + newPtr.getClass().getContainerStore(newPtr).setPtr(newPtr); + } + return newPtr; } bool Creature::isBipedal(const MWWorld::ConstPtr& ptr) const @@ -791,29 +780,26 @@ namespace MWClass const ESM::CreatureState& creatureState = state.asCreatureState(); - if (state.mVersion > 0) + if (!ptr.getRefData().getCustomData()) { - if (!ptr.getRefData().getCustomData()) + if (creatureState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); + else { - if (creatureState.mCreatureStats.mMissingACDT) - ensureCustomData(ptr); + // Create a CustomData, but don't fill it from ESM records (not needed) + auto data = std::make_unique(); + + if (hasInventoryStore(ptr)) + data->mContainerStore = std::make_unique(); else - { - // Create a CustomData, but don't fill it from ESM records (not needed) - auto data = std::make_unique(); + data->mContainerStore = std::make_unique(); - if (hasInventoryStore(ptr)) - data->mContainerStore = std::make_unique(); - else - data->mContainerStore = std::make_unique(); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + data->mContainerStore->setPtr(ptr); - ptr.getRefData().setCustomData(std::move(data)); - } + ptr.getRefData().setCustomData(std::move(data)); } } - else - ensureCustomData( - ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); @@ -833,7 +819,7 @@ namespace MWClass } const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); - if (ptr.getRefData().getCount() <= 0 + if (ptr.getCellRef().getCount() <= 0 && (!isFlagBitSet(ptr, ESM::Creature::Respawn) || !customData.mCreatureStats.isDead())) { state.mHasCustomState = false; @@ -853,7 +839,7 @@ namespace MWClass void Creature::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); - if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) + if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) @@ -865,16 +851,16 @@ namespace MWClass static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay - = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); + = ptr.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (isFlagBitSet(ptr, ESM::Creature::Respawn) && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { - if (ptr.getRefData().getCount() == 0) + if (ptr.getCellRef().getCount() == 0) { - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(1); const ESM::RefId& script = getScript(ptr); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index bd7101e93d..b8619128c2 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -67,7 +67,8 @@ namespace MWClass const osg::Vec3f& hitPosition, bool success) const override; void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const override; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation @@ -78,7 +79,7 @@ namespace MWClass MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; ///< Return inventory store - bool hasInventoryStore(const MWWorld::Ptr& ptr) const override; + bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr @@ -104,9 +105,9 @@ namespace MWClass float getMaxSpeed(const MWWorld::Ptr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; + void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: ///< list getModel(). diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index dd346306e9..f16601531d 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -81,7 +81,7 @@ namespace MWClass if (!creature.isEmpty()) { const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); - if (creature.getRefData().getCount() == 0) + if (creature.getCellRef().getCount() == 0) customData.mSpawn = true; else if (creatureStats.isDead()) { @@ -99,25 +99,6 @@ namespace MWClass customData.mSpawn = true; } - void CreatureLevList::getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const - { - // disable for now, too many false positives - /* - const MWWorld::LiveCellRef *ref = ptr.get(); - for (std::vector::const_iterator it = ref->mBase->mList.begin(); it != - ref->mBase->mList.end(); ++it) - { - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if (it->mLevel > player.getClass().getCreatureStats(player).getLevel()) - continue; - - const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - MWWorld::ManualRef ref(store, it->mId); - ref.getPtr().getClass().getModelsToPreload(ref.getPtr(), models); - } - */ - } - void CreatureLevList::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { @@ -148,7 +129,7 @@ namespace MWClass manualRef.getPtr().getCellRef().setPosition(ptr.getCellRef().getPosition()); manualRef.getPtr().getCellRef().setScale(ptr.getCellRef().getScale()); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject( - manualRef.getPtr(), ptr.getCell(), ptr.getCellRef().getPosition()); + manualRef.getPtr(), ptr.getCell(), ptr.getRefData().getPosition()); customData.mSpawnActorId = placed.getClass().getCreatureStats(placed).getActorId(); customData.mSpawn = false; } diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index d689d1770e..ded8f77de5 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -20,10 +20,6 @@ namespace MWClass bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; - ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: - ///< list getModel(). - void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index ecd6cb59aa..9fe7e92ffa 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -1,6 +1,7 @@ #include "door.hpp" #include +#include #include #include @@ -25,7 +26,6 @@ #include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" @@ -35,6 +35,7 @@ #include "../mwmechanics/actorutil.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -81,7 +82,7 @@ namespace MWClass void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_Door); + physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_Door); } bool Door::isDoor() const @@ -94,17 +95,14 @@ namespace MWClass return true; } - std::string Door::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Door::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Door::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Door::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -136,7 +134,7 @@ namespace MWClass const ESM::MagicEffect* effect = store.get().find(ESM::MagicEffect::Telekinesis); animation->addSpellCastGlow( - effect, 1); // 1 second glow to match the time taken for a door opening or closing + effect->getColor(), 1); // 1 second glow to match the time taken for a door opening or closing } } @@ -267,7 +265,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; @@ -290,10 +288,10 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 17ada40c6f..18dd2348ab 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -51,7 +51,7 @@ namespace MWClass ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; MWWorld::DoorState getDoorState(const MWWorld::ConstPtr& ptr) const override; /// This does not actually cause the door to move. Use World::activateDoor instead. diff --git a/apps/openmw/mwclass/esm4base.cpp b/apps/openmw/mwclass/esm4base.cpp index 956fc210ee..17af545348 100644 --- a/apps/openmw/mwclass/esm4base.cpp +++ b/apps/openmw/mwclass/esm4base.cpp @@ -1,11 +1,11 @@ #include "esm4base.hpp" #include +#include #include #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -29,14 +29,13 @@ namespace MWClass void ESM4Impl::insertObjectPhysics( const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) { - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } MWGui::ToolTipInfo ESM4Impl::getToolTipInfo(std::string_view name, int count) { MWGui::ToolTipInfo info; - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); return info; } } diff --git a/apps/openmw/mwclass/esm4base.hpp b/apps/openmw/mwclass/esm4base.hpp index 3d2184e5fe..f13d6007cd 100644 --- a/apps/openmw/mwclass/esm4base.hpp +++ b/apps/openmw/mwclass/esm4base.hpp @@ -1,14 +1,18 @@ #ifndef GAME_MWCLASS_ESM4BASE_H #define GAME_MWCLASS_ESM4BASE_H +#include #include #include #include +#include "../mwbase/environment.hpp" + #include "../mwgui/tooltips.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/registeredclass.hpp" #include "classmodel.hpp" @@ -23,13 +27,40 @@ namespace MWClass void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics); MWGui::ToolTipInfo getToolTipInfo(std::string_view name, int count); + + // We don't handle ESM4 player stats yet, so for resolving levelled object we use an arbitrary number. + constexpr int sDefaultLevel = 5; + + template + const TargetRecord* resolveLevelled(const ESM::RefId& id, int level = sDefaultLevel) + { + if (id.empty()) + return nullptr; + const MWWorld::ESMStore* esmStore = MWBase::Environment::get().getESMStore(); + const auto& targetStore = esmStore->get(); + const TargetRecord* res = targetStore.search(id); + if (res) + return res; + const LevelledRecord* lvlRec = esmStore->get().search(id); + if (!lvlRec) + return nullptr; + for (const ESM4::LVLO& obj : lvlRec->mLvlObject) + { + ESM::RefId candidateId = ESM::FormId::fromUint32(obj.item); + if (candidateId == id) + continue; + const TargetRecord* candidate = resolveLevelled(candidateId, level); + if (candidate && (!res || obj.level <= level)) + res = candidate; + } + return res; + } } - // Base for all ESM4 Classes + // Base for many ESM4 Classes template class ESM4Base : public MWWorld::Class { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override { const MWWorld::LiveCellRef* ref = ptr.get(); @@ -65,14 +96,14 @@ namespace MWClass std::string_view getName(const MWWorld::ConstPtr& ptr) const override { return {}; } - std::string getModel(const MWWorld::ConstPtr& ptr) const override + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override { - std::string model = getClassModel(ptr); + std::string_view model = getClassModel(ptr); // Hide meshes meshes/marker/* and *LOD.nif in ESM4 cells. It is a temporarty hack. // Needed because otherwise LOD meshes are rendered on top of normal meshes. // TODO: Figure out a better way find markers and LOD meshes; show LOD only outside of active grid. - if (model.empty() || Misc::StringUtils::ciStartsWith(model, "meshes\\marker") + if (model.empty() || Misc::StringUtils::ciStartsWith(model, "marker") || Misc::StringUtils::ciEndsWith(model, "lod.nif")) return {}; @@ -104,58 +135,22 @@ namespace MWClass class ESM4Named : public MWWorld::RegisteredClass, ESM4Base> { public: - friend MWWorld::RegisteredClass>; - ESM4Named() : MWWorld::RegisteredClass>(Record::sRecordId) { } - public: - bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } - - MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override - { - return ESM4Impl::getToolTipInfo(ptr.get()->mBase->mFullName, count); - } - std::string_view getName(const MWWorld::ConstPtr& ptr) const override { return ptr.get()->mBase->mFullName; } - }; - - template - class ESM4Actor : public MWWorld::RegisteredClass, ESM4Base> - { - public: - friend MWWorld::RegisteredClass>; - - ESM4Actor() - : MWWorld::RegisteredClass>(Record::sRecordId) - { - } - - void insertObjectPhysics( - const MWWorld::Ptr&, const std::string&, const osg::Quat&, MWPhysics::PhysicsSystem&) const override - { - } - - bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override { - return ESM4Impl::getToolTipInfo(ptr.get()->mBase->mEditorId, count); + return ESM4Impl::getToolTipInfo(getName(ptr), count); } - std::string getModel(const MWWorld::ConstPtr& ptr) const override - { - // TODO: Not clear where to get something renderable: - // ESM4::Npc::mModel is usually an empty string - // ESM4::Race::mModelMale is only a skeleton - // For now show error marker as a dummy model. - return "meshes/marker_error.nif"; - } + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return !getName(ptr).empty(); } }; } diff --git a/apps/openmw/mwclass/esm4npc.cpp b/apps/openmw/mwclass/esm4npc.cpp new file mode 100644 index 0000000000..a7fb0a3544 --- /dev/null +++ b/apps/openmw/mwclass/esm4npc.cpp @@ -0,0 +1,195 @@ +#include "esm4npc.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + template + static std::vector withBaseTemplates( + const TargetRecord* rec, int level = MWClass::ESM4Impl::sDefaultLevel) + { + std::vector res{ rec }; + while (true) + { + const TargetRecord* newRec + = MWClass::ESM4Impl::resolveLevelled(rec->mBaseTemplate, level); + if (!newRec || newRec == rec) + return res; + res.push_back(rec = newRec); + } + } + + static const ESM4::Npc* chooseTemplate(const std::vector& recs, uint16_t flag) + { + for (const auto* rec : recs) + { + if (rec->mIsTES4) + return rec; + else if (rec->mIsFONV) + { + // TODO: FO3 should use this branch as well. But it is not clear how to distinguish FO3 from + // TES5. Currently FO3 uses wrong template flags that can lead to "ESM4 NPC traits not found" + // exception the NPC will not be added to the scene. But in any way it shouldn't cause a crash. + if (!(rec->mBaseConfig.fo3.templateFlags & flag)) + return rec; + } + else if (rec->mIsFO4) + { + if (!(rec->mBaseConfig.fo4.templateFlags & flag)) + return rec; + } + else if (!(rec->mBaseConfig.tes5.templateFlags & flag)) + return rec; + } + return nullptr; + } + + class ESM4NpcCustomData : public MWWorld::TypedCustomData + { + public: + const ESM4::Npc* mTraits; + const ESM4::Npc* mBaseData; + const ESM4::Race* mRace; + bool mIsFemale; + + // TODO: Use InventoryStore instead (currently doesn't support ESM4 objects) + std::vector mEquippedArmor; + std::vector mEquippedClothing; + + ESM4NpcCustomData& asESM4NpcCustomData() override { return *this; } + const ESM4NpcCustomData& asESM4NpcCustomData() const override { return *this; } + }; + + ESM4NpcCustomData& ESM4Npc::getCustomData(const MWWorld::ConstPtr& ptr) + { + // Note: the argument is ConstPtr because this function is used in `getModel` and `getName` + // which are virtual and work with ConstPtr. `getModel` and `getName` use custom data + // because they require a lot of work including levelled records resolving and it would be + // stupid to not to cache the results. Maybe we should stop using ConstPtr at all + // to avoid such workarounds. + MWWorld::RefData& refData = const_cast(ptr.getRefData()); + + if (auto* data = refData.getCustomData()) + return data->asESM4NpcCustomData(); + + auto data = std::make_unique(); + + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + const ESM4::Npc* const base = ptr.get()->mBase; + auto npcRecs = withBaseTemplates(base); + + data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::Template_UseTraits); + + if (data->mTraits == nullptr) + Log(Debug::Warning) << "Traits are not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; + + data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::Template_UseBaseData); + + if (data->mBaseData == nullptr) + Log(Debug::Warning) << "Base data is not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; + + if (data->mTraits != nullptr) + { + data->mRace = store->get().find(data->mTraits->mRace); + if (data->mTraits->mIsTES4) + data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female; + else if (data->mTraits->mIsFONV) + data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female; + else if (data->mTraits->mIsFO4) + data->mIsFemale + = data->mTraits->mBaseConfig.fo4.flags & ESM4::Npc::TES5_Female; // FO4 flags are the same as TES5 + else + data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female; + } + + if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::Template_UseInventory)) + { + for (const ESM4::InventoryItem& item : inv->mInventory) + { + if (auto* armor + = ESM4Impl::resolveLevelled(ESM::FormId::fromUint32(item.item))) + data->mEquippedArmor.push_back(armor); + else if (data->mTraits != nullptr && data->mTraits->mIsTES4) + { + const auto* clothing = ESM4Impl::resolveLevelled( + ESM::FormId::fromUint32(item.item)); + if (clothing) + data->mEquippedClothing.push_back(clothing); + } + } + if (!inv->mDefaultOutfit.isZeroOrUnset()) + { + if (const ESM4::Outfit* outfit = store->get().search(inv->mDefaultOutfit)) + { + for (ESM::FormId itemId : outfit->mInventory) + if (auto* armor = ESM4Impl::resolveLevelled(itemId)) + data->mEquippedArmor.push_back(armor); + } + else + Log(Debug::Error) << "Outfit not found: " << ESM::RefId(inv->mDefaultOutfit); + } + } + + ESM4NpcCustomData& res = *data; + refData.setCustomData(std::move(data)); + return res; + } + + const std::vector& ESM4Npc::getEquippedArmor(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mEquippedArmor; + } + + const std::vector& ESM4Npc::getEquippedClothing(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mEquippedClothing; + } + + const ESM4::Npc* ESM4Npc::getTraitsRecord(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mTraits; + } + + const ESM4::Race* ESM4Npc::getRace(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mRace; + } + + bool ESM4Npc::isFemale(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mIsFemale; + } + + std::string_view ESM4Npc::getModel(const MWWorld::ConstPtr& ptr) const + { + const ESM4NpcCustomData& data = getCustomData(ptr); + if (data.mTraits == nullptr) + return {}; + if (data.mTraits->mIsTES4) + return data.mTraits->mModel; + return data.mIsFemale ? data.mRace->mModelFemale : data.mRace->mModelMale; + } + + std::string_view ESM4Npc::getName(const MWWorld::ConstPtr& ptr) const + { + const ESM4::Npc* const baseData = getCustomData(ptr).mBaseData; + if (baseData == nullptr) + return {}; + return baseData->mFullName; + } +} diff --git a/apps/openmw/mwclass/esm4npc.hpp b/apps/openmw/mwclass/esm4npc.hpp new file mode 100644 index 0000000000..39116586f1 --- /dev/null +++ b/apps/openmw/mwclass/esm4npc.hpp @@ -0,0 +1,71 @@ +#ifndef GAME_MWCLASS_ESM4ACTOR_H +#define GAME_MWCLASS_ESM4ACTOR_H + +#include +#include + +#include "../mwgui/tooltips.hpp" + +#include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + class ESM4Npc final : public MWWorld::RegisteredClass + { + public: + ESM4Npc() + : MWWorld::RegisteredClass(ESM4::Npc::sRecordId) + { + } + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override + { + const MWWorld::LiveCellRef* ref = ptr.get(); + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override + { + renderingInterface.getObjects().insertNPC(ptr); + } + + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + insertObjectPhysics(ptr, model, rotation, physics); + } + + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + // ESM4Impl::insertObjectPhysics(ptr, getModel(ptr), rotation, physics); + } + + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override + { + return ESM4Impl::getToolTipInfo(getName(ptr), count); + } + + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + + static const ESM4::Npc* getTraitsRecord(const MWWorld::Ptr& ptr); + static const ESM4::Race* getRace(const MWWorld::Ptr& ptr); + static bool isFemale(const MWWorld::Ptr& ptr); + static const std::vector& getEquippedArmor(const MWWorld::Ptr& ptr); + static const std::vector& getEquippedClothing(const MWWorld::Ptr& ptr); + + private: + static ESM4NpcCustomData& getCustomData(const MWWorld::ConstPtr& ptr); + }; +} + +#endif // GAME_MWCLASS_ESM4ACTOR_H diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index e87f74218b..94a1e8cc89 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -1,6 +1,7 @@ #include "ingredient.hpp" #include +#include #include #include @@ -16,12 +17,12 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -39,17 +40,14 @@ namespace MWClass } } - std::string Ingredient::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Ingredient::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Ingredient::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Ingredient::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -107,8 +105,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -118,8 +115,8 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); @@ -144,9 +141,9 @@ namespace MWClass list.push_back(params); } - info.effects = list; + info.effects = std::move(list); - info.text = text; + info.text = std::move(text); info.isIngredient = true; return info; diff --git a/apps/openmw/mwclass/ingredient.hpp b/apps/openmw/mwclass/ingredient.hpp index b7a36d300f..2e7632cd5e 100644 --- a/apps/openmw/mwclass/ingredient.hpp +++ b/apps/openmw/mwclass/ingredient.hpp @@ -47,7 +47,7 @@ namespace MWClass const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 931ed73dfe..9aa5e2c67d 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -1,6 +1,7 @@ #include "light.hpp" #include +#include #include #include @@ -21,12 +22,12 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -62,7 +63,7 @@ namespace MWClass { // TODO: add option somewhere to enable collision for placeable objects if ((ptr.get()->mBase->mData.mFlags & ESM::Light::Carry) == 0) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } bool Light::useAnim() const @@ -70,7 +71,7 @@ namespace MWClass return true; } - std::string Light::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Light::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } @@ -81,9 +82,7 @@ namespace MWClass if (ref->mBase->mModel.empty()) return {}; - - const std::string& name = ref->mBase->mName; - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } bool Light::isItem(const MWWorld::ConstPtr& ptr) const @@ -159,8 +158,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -174,11 +172,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index 70d6852ff8..97625ee5f8 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -69,7 +69,7 @@ namespace MWClass float getRemainingUsageTime(const MWWorld::ConstPtr& ptr) const override; ///< Returns the remaining duration of the object. - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 1fc65c8f79..7955d5af20 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -1,6 +1,7 @@ #include "lockpick.hpp" #include +#include #include #include @@ -15,12 +16,12 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -38,17 +39,14 @@ namespace MWClass } } - std::string Lockpick::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Lockpick::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Lockpick::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Lockpick::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -104,8 +102,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -119,11 +116,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/lockpick.hpp b/apps/openmw/mwclass/lockpick.hpp index fc65a038a6..48c18411a6 100644 --- a/apps/openmw/mwclass/lockpick.hpp +++ b/apps/openmw/mwclass/lockpick.hpp @@ -59,7 +59,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 6c517e3dde..cd97ecf216 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -1,6 +1,7 @@ #include "misc.hpp" #include +#include #include #include @@ -19,12 +20,12 @@ #include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -49,17 +50,14 @@ namespace MWClass } } - std::string Miscellaneous::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Miscellaneous::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Miscellaneous::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Miscellaneous::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -79,8 +77,8 @@ namespace MWClass const MWWorld::LiveCellRef* ref = ptr.get(); int value = ref->mBase->mData.mValue; - if (ptr.getCellRef().getGoldValue() > 1 && ptr.getRefData().getCount() == 1) - value = ptr.getCellRef().getGoldValue(); + if (isGold(ptr) && ptr.getCellRef().getCount() != 1) + value = 1; if (!ptr.getCellRef().getSoul().empty()) { @@ -151,8 +149,8 @@ namespace MWClass countString = " (" + std::to_string(count) + ")"; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) - + MWGui::ToolTips::getCountString(count) + MWGui::ToolTips::getSoulString(ptr.getCellRef()); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count) + + MWGui::ToolTips::getSoulString(ptr.getCellRef()); info.icon = ref->mBase->mIcon; std::string text; @@ -163,11 +161,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } @@ -189,8 +187,7 @@ namespace MWClass const MWWorld::LiveCellRef* ref = newRef.getPtr().get(); MWWorld::Ptr ptr(cell.insert(ref), &cell); - ptr.getCellRef().setGoldValue(goldAmount); - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(goldAmount); return ptr; } @@ -203,7 +200,7 @@ namespace MWClass { const MWWorld::LiveCellRef* ref = ptr.get(); newPtr = MWWorld::Ptr(cell.insert(ref), &cell); - newPtr.getRefData().setCount(count); + newPtr.getCellRef().setCount(count); } newPtr.getCellRef().unsetRefNum(); newPtr.getRefData().setLuaScripts(nullptr); @@ -216,10 +213,9 @@ namespace MWClass MWWorld::Ptr newPtr; if (isGold(ptr)) { - newPtr = createGold(cell, getValue(ptr) * ptr.getRefData().getCount()); + newPtr = createGold(cell, getValue(ptr) * ptr.getCellRef().getCount()); newPtr.getRefData() = ptr.getRefData(); newPtr.getCellRef().setRefNum(ptr.getCellRef().getRefNum()); - newPtr.getRefData().setCount(1); } else { diff --git a/apps/openmw/mwclass/misc.hpp b/apps/openmw/mwclass/misc.hpp index dafeb0c764..6b7b838953 100644 --- a/apps/openmw/mwclass/misc.hpp +++ b/apps/openmw/mwclass/misc.hpp @@ -45,7 +45,7 @@ namespace MWClass const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu diff --git a/apps/openmw/mwclass/nameorid.hpp b/apps/openmw/mwclass/nameorid.hpp new file mode 100644 index 0000000000..b001cc5fc8 --- /dev/null +++ b/apps/openmw/mwclass/nameorid.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_MWCLASS_NAMEORID_H +#define OPENMW_MWCLASS_NAMEORID_H + +#include + +#include "../mwworld/livecellref.hpp" +#include "../mwworld/ptr.hpp" + +#include + +namespace MWClass +{ + template + std::string_view getNameOrId(const MWWorld::ConstPtr& ptr) + { + const MWWorld::LiveCellRef* ref = ptr.get(); + if (!ref->mBase->mName.empty()) + return ref->mBase->mName; + if (const auto* id = ref->mBase->mId.template getIf()) + return id->getValue(); + return {}; + } +} + +#endif diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index b9e03aa45c..22c953d27d 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1,7 +1,9 @@ #include "npc.hpp" #include +#include +#include #include #include @@ -17,6 +19,7 @@ #include #include #include +#include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" @@ -57,10 +60,28 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" + +#include "nameorid.hpp" namespace { + struct NpcParts + { + const ESM::RefId mSwimLeft = ESM::RefId::stringRefId("Swim Left"); + const ESM::RefId mSwimRight = ESM::RefId::stringRefId("Swim Right"); + const ESM::RefId mFootWaterLeft = ESM::RefId::stringRefId("FootWaterLeft"); + const ESM::RefId mFootWaterRight = ESM::RefId::stringRefId("FootWaterRight"); + const ESM::RefId mFootBareLeft = ESM::RefId::stringRefId("FootBareLeft"); + const ESM::RefId mFootBareRight = ESM::RefId::stringRefId("FootBareRight"); + const ESM::RefId mFootLightLeft = ESM::RefId::stringRefId("footLightLeft"); + const ESM::RefId mFootLightRight = ESM::RefId::stringRefId("footLightRight"); + const ESM::RefId mFootMediumRight = ESM::RefId::stringRefId("FootMedRight"); + const ESM::RefId mFootMediumLeft = ESM::RefId::stringRefId("FootMedLeft"); + const ESM::RefId mFootHeavyLeft = ESM::RefId::stringRefId("footHeavyLeft"); + const ESM::RefId mFootHeavyRight = ESM::RefId::stringRefId("footHeavyRight"); + }; + + const NpcParts npcParts; int is_even(double d) { @@ -92,11 +113,7 @@ namespace const auto& attributes = MWBase::Environment::get().getESMStore()->get(); int level = creatureStats.getLevel(); for (const ESM::Attribute& attribute : attributes) - { - const ESM::Race::MaleFemale& value - = race->mData.mAttributeValues[static_cast(ESM::Attribute::refIdToIndex(attribute.mId))]; - creatureStats.setAttribute(attribute.mId, male ? value.mMale : value.mFemale); - } + creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); // class bonus const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); @@ -297,6 +314,7 @@ namespace MWClass { if (!ptr.getRefData().getCustomData()) { + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); bool recalculate = false; auto tempData = std::make_unique(); NpcCustomData* data = tempData.get(); @@ -316,14 +334,8 @@ namespace MWClass for (size_t i = 0; i < ref->mBase->mNpdt.mSkills.size(); ++i) data->mNpcStats.getSkill(ESM::Skill::indexToRefId(i)).setBase(ref->mBase->mNpdt.mSkills[i]); - data->mNpcStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mNpdt.mStrength); - data->mNpcStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mNpdt.mIntelligence); - data->mNpcStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mNpdt.mWillpower); - data->mNpcStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mNpdt.mAgility); - data->mNpcStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mNpdt.mSpeed); - data->mNpcStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mNpdt.mEndurance); - data->mNpcStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mNpdt.mPersonality); - data->mNpcStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mNpdt.mLuck); + for (size_t i = 0; i < ref->mBase->mNpdt.mAttributes.size(); ++i) + data->mNpcStats.setAttribute(ESM::Attribute::indexToRefId(i), ref->mBase->mNpdt.mAttributes[i]); data->mNpcStats.setHealth(ref->mBase->mNpdt.mHealth); data->mNpcStats.setMagicka(ref->mBase->mNpdt.mMana); @@ -397,9 +409,10 @@ namespace MWClass // inventory // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - getInventoryStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId(), prng); - - getInventoryStore(ptr).autoEquip(); + MWWorld::InventoryStore& inventory = getInventoryStore(ptr); + inventory.setPtr(ptr); + inventory.fill(ref->mBase->mInventory, ptr.getCellRef().getRefId(), prng); + inventory.autoEquip(); } } @@ -415,101 +428,105 @@ namespace MWClass return (ref->mBase->mRecordFlags & ESM::FLAG_Persistent) != 0; } - std::string Npc::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Npc::getModel(const MWWorld::ConstPtr& ptr) const + { + const MWWorld::LiveCellRef* ref = ptr.get(); + std::string_view model = Settings::models().mBaseanim.get(); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mRace); + if (race->mData.mFlags & ESM::Race::Beast) + model = Settings::models().mBaseanimkna.get(); + // Base animations should be in the meshes dir + constexpr std::string_view prefix = "meshes/"; + assert(VFS::Path::pathEqual(prefix, model.substr(0, prefix.size()))); + return model.substr(prefix.size()); + } + + VFS::Path::Normalized Npc::getCorrectedModel(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); - std::string model = Settings::Manager::getString("baseanim", "Models"); const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mRace); if (race->mData.mFlags & ESM::Race::Beast) - model = Settings::Manager::getString("baseanimkna", "Models"); + return Settings::models().mBaseanimkna.get(); - return model; + return Settings::models().mBaseanim.get(); } - void Npc::getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const + void Npc::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { const MWWorld::LiveCellRef* npc = ptr.get(); - const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mRace); - if (race && race->mData.mFlags & ESM::Race::Beast) - models.emplace_back(Settings::Manager::getString("baseanimkna", "Models")); - - // keep these always loaded just in case - models.emplace_back(Settings::Manager::getString("xargonianswimkna", "Models")); - models.emplace_back(Settings::Manager::getString("xbaseanimfemale", "Models")); - models.emplace_back(Settings::Manager::getString("xbaseanim", "Models")); - - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + const auto& esmStore = MWBase::Environment::get().getESMStore(); + models.push_back(getModel(ptr)); if (!npc->mBase->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(npc->mBase->mModel, vfs)); + models.push_back(npc->mBase->mModel); if (!npc->mBase->mHead.empty()) { - const ESM::BodyPart* head - = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mHead); + const ESM::BodyPart* head = esmStore->get().search(npc->mBase->mHead); if (head) - models.push_back(Misc::ResourceHelpers::correctMeshPath(head->mModel, vfs)); + models.push_back(head->mModel); } if (!npc->mBase->mHair.empty()) { - const ESM::BodyPart* hair - = MWBase::Environment::get().getESMStore()->get().search(npc->mBase->mHair); + const ESM::BodyPart* hair = esmStore->get().search(npc->mBase->mHair); if (hair) - models.push_back(Misc::ResourceHelpers::correctMeshPath(hair->mModel, vfs)); + models.push_back(hair->mModel); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); - // FIXME: use const version of InventoryStore functions once they are available - // preload equipped items - const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); - for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); + if (customData) { - MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); - if (equipped != invStore.end()) + const MWWorld::InventoryStore& invStore = customData->asNpcCustomData().mInventoryStore; + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { - std::vector parts; - if (equipped->getType() == ESM::Clothing::sRecordId) + MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); + if (equipped != invStore.end()) { - const ESM::Clothing* clothes = equipped->get()->mBase; - parts = clothes->mParts.mParts; - } - else if (equipped->getType() == ESM::Armor::sRecordId) - { - const ESM::Armor* armor = equipped->get()->mBase; - parts = armor->mParts.mParts; - } - else - { - std::string model = equipped->getClass().getModel(*equipped); - if (!model.empty()) - models.push_back(model); - } + const auto addParts = [&](const std::vector& parts) { + for (const ESM::PartReference& partRef : parts) + { + const ESM::RefId& partname + = (female && !partRef.mFemale.empty()) || (!female && partRef.mMale.empty()) + ? partRef.mFemale + : partRef.mMale; - for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) - { - const ESM::RefId& partname - = (female && !it->mFemale.empty()) || (!female && it->mMale.empty()) ? it->mFemale : it->mMale; - - const ESM::BodyPart* part - = MWBase::Environment::get().getESMStore()->get().search(partname); - if (part && !part->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel, vfs)); + const ESM::BodyPart* part = esmStore->get().search(partname); + if (part && !part->mModel.empty()) + models.push_back(part->mModel); + } + }; + if (equipped->getType() == ESM::Clothing::sRecordId) + { + const ESM::Clothing* clothes = equipped->get()->mBase; + addParts(clothes->mParts.mParts); + } + else if (equipped->getType() == ESM::Armor::sRecordId) + { + const ESM::Armor* armor = equipped->get()->mBase; + addParts(armor->mParts.mParts); + } + else + { + std::string_view model = equipped->getClass().getModel(*equipped); + if (!model.empty()) + models.push_back(model); + } } } } // preload body parts - if (race) + if (const ESM::Race* race = esmStore->get().search(npc->mBase->mRace)) { const std::vector& parts = MWRender::NpcAnimation::getBodyParts(race->mId, female, false, false); - for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) + for (const ESM::BodyPart* part : parts) { - const ESM::BodyPart* part = *it; if (part && !part->mModel.empty()) - models.push_back(Misc::ResourceHelpers::correctMeshPath(part->mModel, vfs)); + models.push_back(part->mModel); } } } @@ -525,10 +542,7 @@ namespace MWClass return store.find("sWerewolfPopup")->mValue.getString(); } - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } MWMechanics::CreatureStats& Npc::getCreatureStats(const MWWorld::Ptr& ptr) const @@ -564,25 +578,10 @@ namespace MWClass * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach : store.find("fHandToHandReach")->mValue.getFloat()); - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit - // result. - std::vector targetActors; - if (ptr != MWMechanics::getPlayer()) - getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors); - - // TODO: Use second to work out the hit angle - std::pair result = world->getHitContact(ptr, dist, targetActors); + const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; - const MWWorld::Class& othercls = result.first.getClass(); - if (!othercls.isActor()) // Can't hit non-actors - return true; - - MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(result.first); - if (otherstats.isDead()) // Can't hit dead actors - return true; - // Note that earlier we returned true in spite of an apparent failure to hit anything alive. // This is because hitting nothing is not a "miss" and should be handled as such character controller-side. victim = result.first; @@ -622,7 +621,8 @@ namespace MWClass float damage = 0.0f; if (!success) { - othercls.onHit(victim, damage, false, weapon, ptr, osg::Vec3f(), false); + othercls.onHit( + victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); return; @@ -633,11 +633,11 @@ namespace MWClass { const unsigned char* attack = nullptr; if (type == ESM::Weapon::AT_Chop) - attack = weapon.get()->mBase->mData.mChop; + attack = weapon.get()->mBase->mData.mChop.data(); else if (type == ESM::Weapon::AT_Slash) - attack = weapon.get()->mBase->mData.mSlash; + attack = weapon.get()->mBase->mData.mSlash.data(); else if (type == ESM::Weapon::AT_Thrust) - attack = weapon.get()->mBase->mData.mThrust; + attack = weapon.get()->mBase->mData.mThrust.data(); if (attack) { damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); @@ -661,7 +661,7 @@ namespace MWClass ESM::RefId weapskill = ESM::Skill::HandToHand; if (!weapon.isEmpty()) weapskill = weapon.getClass().getEquipmentSkill(weapon); - skillUsageSucceeded(ptr, weapskill, 0); + skillUsageSucceeded(ptr, weapskill, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& seq = victim.getClass().getCreatureStats(victim).getAiSequence(); @@ -671,8 +671,11 @@ namespace MWClass { damage *= store.find("fCombatCriticalStrikeMult")->mValue.getFloat(); MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); - MWBase::Environment::get().getSoundManager()->playSound3D( - victim, ESM::RefId::stringRefId("critical damage"), 1.0f, 1.0f); + if (healthdmg) + { + MWBase::Environment::get().getSoundManager()->playSound3D( + victim, ESM::RefId::stringRefId("critical damage"), 1.0f, 1.0f); + } } } @@ -685,34 +688,36 @@ namespace MWClass MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) - { damage = 0; - victim.getClass().block(victim); - } if (victim == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) damage = 0; MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); - // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) { stats.setAttacked(true); - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } // Attacker and target store each other as hitattemptactor if they have no one stored yet @@ -752,9 +757,6 @@ namespace MWClass if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); - if (damage > 0.0f && !object.isEmpty()) - MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); - if (damage < 0.001f) damage = 0; @@ -849,7 +851,7 @@ namespace MWClass ESM::RefId skill = armor.getClass().getEquipmentSkill(armor); if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, skill, 0); + skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent); if (skill == ESM::Skill::LightArmor) sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); @@ -859,7 +861,7 @@ namespace MWClass sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); } else if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, ESM::Skill::Unarmored, 0); + skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent); } } @@ -921,53 +923,51 @@ namespace MWClass } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + const MWMechanics::AiSequence& aiSequence = stats.getAiSequence(); + const bool isPursuing = aiSequence.isInPursuit() && actor == MWMechanics::getPlayer(); + const bool inCombatWithActor = aiSequence.isInCombat(actor) || isPursuing; if (stats.isDead()) { - // by default user can loot friendly actors during death animation - if (Settings::game().mCanLootDuringDeathAnimation && !stats.getAiSequence().isInCombat()) + // by default user can loot non-fighting actors during death animation + if (Settings::game().mCanLootDuringDeathAnimation) return std::make_unique(ptr); // otherwise wait until death animation if (stats.isDeathAnimationFinished()) return std::make_unique(ptr); } - else if (!stats.getAiSequence().isInCombat()) + else { - if (stats.getKnockedDown() || MWBase::Environment::get().getMechanicsManager()->isSneaking(actor)) - return std::make_unique(ptr); // stealing + const bool allowStealingFromKO + = Settings::game().mAlwaysAllowStealingFromKnockedOutActors || !inCombatWithActor; + if (stats.getKnockedDown() && allowStealingFromKO) + return std::make_unique(ptr); - // Can't talk to werewolves - if (!getNpcStats(ptr).isWerewolf()) + const bool allowStealingWhileSneaking = !inCombatWithActor; + if (MWBase::Environment::get().getMechanicsManager()->isSneaking(actor) && allowStealingWhileSneaking) + return std::make_unique(ptr); + + const bool allowTalking = !inCombatWithActor && !getNpcStats(ptr).isWerewolf(); + if (allowTalking) return std::make_unique(ptr); } - else // In combat - { - if (Settings::game().mAlwaysAllowStealingFromKnockedOutActors && stats.getKnockedDown()) - return std::make_unique(ptr); // stealing - } - // Tribunal and some mod companions oddly enough must use open action as fallback - if (!getScript(ptr).empty() && ptr.getRefData().getLocals().getIntVar(getScript(ptr), "companion")) - return std::make_unique(ptr); + if (inCombatWithActor) + return std::make_unique("#{sActorInCombat}"); return std::make_unique(); } MWWorld::ContainerStore& Npc::getContainerStore(const MWWorld::Ptr& ptr) const { - ensureCustomData(ptr); - auto& store = ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; - store.setActor(ptr); - return store; + return getInventoryStore(ptr); } MWWorld::InventoryStore& Npc::getInventoryStore(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); - auto& store = ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; - store.setActor(ptr); - return store; + return ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; } ESM::RefId Npc::getScript(const MWWorld::ConstPtr& ptr) const @@ -982,8 +982,7 @@ namespace MWClass // TODO: This function is called several times per frame for each NPC. // It would be better to calculate it only once per frame for each NPC and save the result in CreatureStats. const MWMechanics::NpcStats& stats = getNpcStats(ptr); - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; const MWBase::World* world = MWBase::Environment::get().getWorld(); @@ -1032,8 +1031,7 @@ namespace MWClass return 0.f; const MWMechanics::NpcStats& stats = getNpcStats(ptr); - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; const GMST& gmst = getGmst(); @@ -1088,7 +1086,8 @@ namespace MWClass if (customData.mNpcStats.isDead() && customData.mNpcStats.isDeathAnimationFinished()) return true; - if (!customData.mNpcStats.getAiSequence().isInCombat()) + const MWMechanics::AiSequence& aiSeq = customData.mNpcStats.getAiSequence(); + if (!aiSeq.isInCombat() || aiSeq.isFleeing()) return true; if (Settings::game().mAlwaysAllowStealingFromKnockedOutActors && customData.mNpcStats.getKnockedDown()) @@ -1105,7 +1104,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); if (fullHelp && !ref->mBase->mName.empty() && ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { @@ -1115,7 +1114,7 @@ namespace MWClass } if (fullHelp) - info.text = MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra = MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); return info; } @@ -1138,29 +1137,9 @@ namespace MWClass return getNpcStats(ptr).isWerewolf() ? 0.0f : Actor::getEncumbrance(ptr); } - bool Npc::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const - { - MWBase::Environment::get().getWorld()->breakInvisibility(actor); - MWMechanics::CastSpell cast(actor, actor); - const ESM::RefId& recordId = consumable.getCellRef().getRefId(); - MWBase::Environment::get().getWorldModel()->registerPtr(consumable); - MWBase::Environment::get().getLuaManager()->itemConsumed(consumable, actor); - actor.getClass().getContainerStore(actor).remove(consumable, 1); - return cast.cast(recordId); - } - void Npc::skillUsageSucceeded(const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor) const { - MWMechanics::NpcStats& stats = getNpcStats(ptr); - - if (stats.isWerewolf()) - return; - - MWWorld::LiveCellRef* ref = ptr.get(); - - const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mClass); - - stats.useSkill(skill, *class_, usageType, extraFactor); + MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); } float Npc::getArmorRating(const MWWorld::Ptr& ptr) const @@ -1226,24 +1205,24 @@ namespace MWClass if (ptr == MWMechanics::getPlayer() && ptr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson()) { if (ref->mBase->isMale()) - scale *= race->mData.mHeight.mMale; + scale *= race->mData.mMaleHeight; else - scale *= race->mData.mHeight.mFemale; + scale *= race->mData.mFemaleHeight; return; } if (ref->mBase->isMale()) { - scale.x() *= race->mData.mWeight.mMale; - scale.y() *= race->mData.mWeight.mMale; - scale.z() *= race->mData.mHeight.mMale; + scale.x() *= race->mData.mMaleWeight; + scale.y() *= race->mData.mMaleWeight; + scale.z() *= race->mData.mMaleHeight; } else { - scale.x() *= race->mData.mWeight.mFemale; - scale.y() *= race->mData.mWeight.mFemale; - scale.z() *= race->mData.mHeight.mFemale; + scale.x() *= race->mData.mFemaleWeight; + scale.y() *= race->mData.mFemaleWeight; + scale.z() *= race->mData.mFemaleHeight; } } @@ -1260,19 +1239,6 @@ namespace MWClass ESM::RefId Npc::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { - static const ESM::RefId swimLeft = ESM::RefId::stringRefId("Swim Left"); - static const ESM::RefId swimRight = ESM::RefId::stringRefId("Swim Right"); - static const ESM::RefId footWaterLeft = ESM::RefId::stringRefId("FootWaterLeft"); - static const ESM::RefId footWaterRight = ESM::RefId::stringRefId("FootWaterRight"); - static const ESM::RefId footBareLeft = ESM::RefId::stringRefId("FootBareLeft"); - static const ESM::RefId footBareRight = ESM::RefId::stringRefId("FootBareRight"); - static const ESM::RefId footLightLeft = ESM::RefId::stringRefId("footLightLeft"); - static const ESM::RefId footLightRight = ESM::RefId::stringRefId("footLightRight"); - static const ESM::RefId footMediumRight = ESM::RefId::stringRefId("FootMedRight"); - static const ESM::RefId footMediumLeft = ESM::RefId::stringRefId("FootMedLeft"); - static const ESM::RefId footHeavyLeft = ESM::RefId::stringRefId("footHeavyLeft"); - static const ESM::RefId footHeavyRight = ESM::RefId::stringRefId("footHeavyRight"); - if (name == "left" || name == "right") { MWBase::World* world = MWBase::Environment::get().getWorld(); @@ -1280,9 +1246,9 @@ namespace MWClass return ESM::RefId(); osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); if (world->isSwimming(ptr)) - return (name == "left") ? swimLeft : swimRight; + return (name == "left") ? npcParts.mSwimLeft : npcParts.mSwimRight; if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) - return (name == "left") ? footWaterLeft : footWaterRight; + return (name == "left") ? npcParts.mFootWaterLeft : npcParts.mFootWaterRight; if (world->isOnGround(ptr)) { if (getNpcStats(ptr).isWerewolf() @@ -1297,15 +1263,15 @@ namespace MWClass const MWWorld::InventoryStore& inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if (boots == inv.end() || boots->getType() != ESM::Armor::sRecordId) - return (name == "left") ? footBareLeft : footBareRight; + return (name == "left") ? npcParts.mFootBareLeft : npcParts.mFootBareRight; ESM::RefId skill = boots->getClass().getEquipmentSkill(*boots); if (skill == ESM::Skill::LightArmor) - return (name == "left") ? footLightLeft : footLightRight; + return (name == "left") ? npcParts.mFootLightLeft : npcParts.mFootLightRight; else if (skill == ESM::Skill::MediumArmor) - return (name == "left") ? footMediumLeft : footMediumRight; + return (name == "left") ? npcParts.mFootMediumLeft : npcParts.mFootMediumRight; else if (skill == ESM::Skill::HeavyArmor) - return (name == "left") ? footHeavyLeft : footHeavyRight; + return (name == "left") ? npcParts.mFootHeavyLeft : npcParts.mFootHeavyRight; } return ESM::RefId(); } @@ -1314,9 +1280,9 @@ namespace MWClass if (name == "land") return ESM::RefId(); if (name == "swimleft") - return swimLeft; + return npcParts.mSwimLeft; if (name == "swimright") - return swimRight; + return npcParts.mSwimRight; // TODO: I have no idea what these are supposed to do for NPCs since they use // voiced dialog for various conditions like health loss and combat taunts. Maybe // only for biped creatures? @@ -1334,8 +1300,13 @@ namespace MWClass MWWorld::Ptr Npc::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); - - return MWWorld::Ptr(cell.insert(ref), &cell); + MWWorld::Ptr newPtr(cell.insert(ref), &cell); + if (newPtr.getRefData().getCustomData()) + { + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + newPtr.getClass().getContainerStore(newPtr).setPtr(newPtr); + } + return newPtr; } float Npc::getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const @@ -1355,20 +1326,19 @@ namespace MWClass const ESM::NpcState& npcState = state.asNpcState(); - if (state.mVersion > 0) + if (!ptr.getRefData().getCustomData()) { - if (!ptr.getRefData().getCustomData()) + if (npcState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); + else { - if (npcState.mCreatureStats.mMissingACDT) - ensureCustomData(ptr); - else - // Create a CustomData, but don't fill it from ESM records (not needed) - ptr.getRefData().setCustomData(std::make_unique()); + // Create a CustomData, but don't fill it from ESM records (not needed) + auto data = std::make_unique(); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + data->mInventoryStore.setPtr(ptr); + ptr.getRefData().setCustomData(std::move(data)); } } - else - ensureCustomData( - ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); @@ -1389,7 +1359,7 @@ namespace MWClass } const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); - if (ptr.getRefData().getCount() <= 0 + if (ptr.getCellRef().getCount() <= 0 && (!(ptr.get()->mBase->mFlags & ESM::NPC::Respawn) || !customData.mNpcStats.isDead())) { state.mHasCustomState = false; @@ -1426,7 +1396,7 @@ namespace MWClass void Npc::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); - if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) + if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) @@ -1438,16 +1408,16 @@ namespace MWClass static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay - = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); + = ptr.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (ptr.get()->mBase->mFlags & ESM::NPC::Respawn && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { - if (ptr.getRefData().getCount() == 0) + if (ptr.getCellRef().getCount() == 0) { - ptr.getRefData().setCount(1); + ptr.getCellRef().setCount(1); const ESM::RefId& script = getScript(ptr); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 9b53143c4d..29ab459242 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -74,7 +74,7 @@ namespace MWClass MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; ///< Return inventory store - bool hasInventoryStore(const MWWorld::Ptr& ptr) const override { return true; } + bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override { return true; } bool evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const override; @@ -82,9 +82,10 @@ namespace MWClass const osg::Vec3f& hitPosition, bool success) const override; void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const override; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; + void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: ///< list getModel(). @@ -114,8 +115,6 @@ namespace MWClass float getArmorRating(const MWWorld::Ptr& ptr) const override; ///< @return combined armor rating of this actor - bool consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const override; - void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh @@ -132,7 +131,9 @@ namespace MWClass ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; + + VFS::Path::Normalized getCorrectedModel(const MWWorld::ConstPtr& ptr) const override; float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 9bab0345cb..9dca460724 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -1,6 +1,7 @@ #include "potion.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,14 +14,15 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/alchemy.hpp" +#include "../mwmechanics/spellutil.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -38,17 +40,14 @@ namespace MWClass } } - std::string Potion::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Potion::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Potion::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Potion::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -65,9 +64,7 @@ namespace MWClass int Potion::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - - return ref->mBase->mData.mValue; + return MWMechanics::getPotionValue(*ptr.get()->mBase); } const ESM::RefId& Potion::getUpSoundId(const MWWorld::ConstPtr& ptr) const @@ -95,14 +92,13 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); + text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); info.effects = MWGui::Widgets::MWEffectList::effectListFromESM(&ref->mBase->mEffects); @@ -115,11 +111,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/potion.hpp b/apps/openmw/mwclass/potion.hpp index 66b11b8ef4..057874ca1e 100644 --- a/apps/openmw/mwclass/potion.hpp +++ b/apps/openmw/mwclass/potion.hpp @@ -47,7 +47,7 @@ namespace MWClass const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index e020c89443..4039f72cc5 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -1,6 +1,7 @@ #include "probe.hpp" #include +#include #include #include @@ -15,12 +16,12 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -38,17 +39,14 @@ namespace MWClass } } - std::string Probe::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Probe::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Probe::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Probe::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { @@ -103,8 +101,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -118,11 +115,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/probe.hpp b/apps/openmw/mwclass/probe.hpp index 9a66d7a4bf..fc1092546e 100644 --- a/apps/openmw/mwclass/probe.hpp +++ b/apps/openmw/mwclass/probe.hpp @@ -54,7 +54,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index 68fc2f60da..922b33b67e 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -1,6 +1,7 @@ #include "repair.hpp" #include +#include #include #include @@ -13,12 +14,12 @@ #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -36,17 +37,14 @@ namespace MWClass } } - std::string Repair::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Repair::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Repair::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Repair::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -105,8 +103,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -120,11 +117,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } diff --git a/apps/openmw/mwclass/repair.hpp b/apps/openmw/mwclass/repair.hpp index ee96c83eeb..50c58231ce 100644 --- a/apps/openmw/mwclass/repair.hpp +++ b/apps/openmw/mwclass/repair.hpp @@ -44,7 +44,7 @@ namespace MWClass const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu (default implementation: return a diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index fe1226f19b..65fe58ea76 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -40,10 +40,10 @@ namespace MWClass void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } - std::string Static::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Static::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index 65f60f7653..a3ea68a86f 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -32,7 +32,7 @@ namespace MWClass bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 68a66b69d9..089c8c2894 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -1,6 +1,7 @@ #include "weapon.hpp" #include +#include #include #include @@ -20,12 +21,12 @@ #include "../mwmechanics/weapontype.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwgui/ustring.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { @@ -43,17 +44,14 @@ namespace MWClass } } - std::string Weapon::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Weapon::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Weapon::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId.getRefIdString(); + return getNameOrId(ptr); } std::unique_ptr Weapon::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const @@ -150,8 +148,7 @@ namespace MWClass MWGui::ToolTipInfo info; std::string_view name = getName(ptr); - info.caption - = MyGUI::TextIterator::toTagsString(MWGui::toUString(name)) + MWGui::ToolTips::getCountString(count); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -240,11 +237,11 @@ namespace MWClass if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } @@ -277,8 +274,8 @@ namespace MWClass return { 0, "#{sInventoryMessage1}" }; // Do not allow equip weapons from inventory during attack - if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) - && MWBase::Environment::get().getWindowManager()->isGuiMode()) + if (npc.isInCell() && MWBase::Environment::get().getWindowManager()->isGuiMode() + && MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc)) return { 0, "#{sCantEquipWeapWarning}" }; std::pair, bool> slots_ = getEquipmentSlots(ptr); diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index 110069341d..9e79532bc0 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -72,7 +72,7 @@ namespace MWClass std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 2b7774d8c9..9f3bb0b26f 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -543,7 +543,8 @@ namespace MWDialogue mPermanentDispositionChange += perm; MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().skillUsageSucceeded(player, ESM::Skill::Speechcraft, success ? 0 : 1); + player.getClass().skillUsageSucceeded( + player, ESM::Skill::Speechcraft, success ? ESM::Skill::Speechcraft_Success : ESM::Skill::Speechcraft_Fail); if (success) { @@ -619,25 +620,25 @@ namespace MWDialogue return false; } - void DialogueManager::say(const MWWorld::Ptr& actor, const ESM::RefId& topic) + bool DialogueManager::say(const MWWorld::Ptr& actor, const ESM::RefId& topic) { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (sndMgr->sayActive(actor)) { // Actor is already saying something. - return; + return false; } if (actor.getClass().isNpc() && MWBase::Environment::get().getWorld()->isSwimming(actor)) { // NPCs don't talk while submerged - return; + return false; } if (actor.getClass().getCreatureStats(actor).getKnockedDown()) { // Unconscious actors can not speak - return; + return false; } const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -652,10 +653,11 @@ namespace MWDialogue if (Settings::gui().mSubtitles) winMgr->messageBox(info->mResponse); if (!info->mSound.empty()) - sndMgr->say(actor, Misc::ResourceHelpers::correctSoundPath(info->mSound)); + sndMgr->say(actor, Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(info->mSound))); if (!info->mResultScript.empty()) executeScript(info->mResultScript, actor); } + return info != nullptr; } int DialogueManager::countSavedGameRecords() const @@ -737,6 +739,17 @@ namespace MWDialogue return 0; } + const std::map* DialogueManager::getFactionReactionOverrides(const ESM::RefId& faction) const + { + // Make sure the faction exists + MWBase::Environment::get().getESMStore()->get().find(faction); + + const auto found = mChangedFactionReaction.find(faction); + if (found != mChangedFactionReaction.end()) + return &found->second; + return nullptr; + } + void DialogueManager::clearInfoActor(const MWWorld::Ptr& actor) const { if (actor == mActor && !mLastTopic.empty()) diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index c214106fdc..a735c57fef 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -100,7 +100,7 @@ namespace MWDialogue bool checkServiceRefused(ResponseCallback* callback, ServiceType service = ServiceType::Any) override; - void say(const MWWorld::Ptr& actor, const ESM::RefId& topic) override; + bool say(const MWWorld::Ptr& actor, const ESM::RefId& topic) override; // calbacks for the GUI void keywordSelected(std::string_view keyword, ResponseCallback* callback) override; @@ -126,6 +126,8 @@ namespace MWDialogue /// @return faction1's opinion of faction2 int getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2) const override; + const std::map* getFactionReactionOverrides(const ESM::RefId& faction) const override; + /// Removes the last added topic response for the given actor from the journal void clearInfoActor(const MWWorld::Ptr& actor) const override; }; diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index 899acf603e..295d690ce5 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -30,16 +30,16 @@ namespace { bool matchesStaticFilters(const MWDialogue::SelectWrapper& select, const MWWorld::Ptr& actor) { - const ESM::RefId selectId = ESM::RefId::stringRefId(select.getName()); - if (select.getFunction() == MWDialogue::SelectWrapper::Function_NotId) + const ESM::RefId selectId = select.getId(); + if (select.getFunction() == ESM::DialogueCondition::Function_NotId) return actor.getCellRef().getRefId() != selectId; if (actor.getClass().isNpc()) { - if (select.getFunction() == MWDialogue::SelectWrapper::Function_NotFaction) + if (select.getFunction() == ESM::DialogueCondition::Function_NotFaction) return actor.getClass().getPrimaryFaction(actor) != selectId; - else if (select.getFunction() == MWDialogue::SelectWrapper::Function_NotClass) + else if (select.getFunction() == ESM::DialogueCondition::Function_NotClass) return actor.get()->mBase->mClass != selectId; - else if (select.getFunction() == MWDialogue::SelectWrapper::Function_NotRace) + else if (select.getFunction() == ESM::DialogueCondition::Function_NotRace) return actor.get()->mBase->mRace != selectId; } return true; @@ -47,7 +47,7 @@ namespace bool matchesStaticFilters(const ESM::DialInfo& info, const MWWorld::Ptr& actor) { - for (const ESM::DialInfo::SelectStruct& select : info.mSelects) + for (const auto& select : info.mSelects) { MWDialogue::SelectWrapper wrapper = select; if (wrapper.getType() == MWDialogue::SelectWrapper::Type_Boolean) @@ -62,7 +62,7 @@ namespace } else if (wrapper.getType() == MWDialogue::SelectWrapper::Type_Numeric) { - if (wrapper.getFunction() == MWDialogue::SelectWrapper::Function_Local) + if (wrapper.getFunction() == ESM::DialogueCondition::Function_Local) { const ESM::RefId& scriptName = actor.getClass().getScript(actor); if (scriptName.empty()) @@ -207,9 +207,8 @@ bool MWDialogue::Filter::testPlayer(const ESM::DialInfo& info) const bool MWDialogue::Filter::testSelectStructs(const ESM::DialInfo& info) const { - for (std::vector::const_iterator iter(info.mSelects.begin()); - iter != info.mSelects.end(); ++iter) - if (!testSelectStruct(*iter)) + for (const auto& select : info.mSelects) + if (!testSelectStruct(select)) return false; return true; @@ -270,11 +269,11 @@ bool MWDialogue::Filter::testSelectStruct(const SelectWrapper& select) const // If the actor is a creature, we pass all conditions only applicable to NPCs. return true; - if (select.getFunction() == SelectWrapper::Function_Choice && mChoice == -1) + if (select.getFunction() == ESM::DialogueCondition::Function_Choice && mChoice == -1) // If not currently in a choice, we reject all conditions that test against choices. return false; - if (select.getFunction() == SelectWrapper::Function_Weather + if (select.getFunction() == ESM::DialogueCondition::Function_Weather && !(MWBase::Environment::get().getWorld()->isCellExterior() || MWBase::Environment::get().getWorld()->isCellQuasiExterior())) // Reject weather conditions in interior cells @@ -305,29 +304,31 @@ bool MWDialogue::Filter::testSelectStructNumeric(const SelectWrapper& select) co { switch (select.getFunction()) { - case SelectWrapper::Function_Global: + case ESM::DialogueCondition::Function_Global: // internally all globals are float :( return select.selectCompare(MWBase::Environment::get().getWorld()->getGlobalFloat(select.getName())); - case SelectWrapper::Function_Local: + case ESM::DialogueCondition::Function_Local: { return testFunctionLocal(select); } - case SelectWrapper::Function_NotLocal: + case ESM::DialogueCondition::Function_NotLocal: { return !testFunctionLocal(select); } - case SelectWrapper::Function_PcHealthPercent: + case ESM::DialogueCondition::Function_PcHealthPercent: { MWWorld::Ptr player = MWMechanics::getPlayer(); return select.selectCompare( static_cast(player.getClass().getCreatureStats(player).getHealth().getRatio() * 100)); } - case SelectWrapper::Function_PcDynamicStat: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: { MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -336,7 +337,7 @@ bool MWDialogue::Filter::testSelectStructNumeric(const SelectWrapper& select) co return select.selectCompare(value); } - case SelectWrapper::Function_HealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: { return select.selectCompare( static_cast(mActor.getClass().getCreatureStats(mActor).getHealth().getRatio() * 100)); @@ -354,59 +355,100 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons switch (select.getFunction()) { - case SelectWrapper::Function_Journal: + case ESM::DialogueCondition::Function_Journal: - return MWBase::Environment::get().getJournal()->getJournalIndex(ESM::RefId::stringRefId(select.getName())); + return MWBase::Environment::get().getJournal()->getJournalIndex(select.getId()); - case SelectWrapper::Function_Item: + case ESM::DialogueCondition::Function_Item: { MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); - return store.count(ESM::RefId::stringRefId(select.getName())); + return store.count(select.getId()); } - case SelectWrapper::Function_Dead: + case ESM::DialogueCondition::Function_Dead: - return MWBase::Environment::get().getMechanicsManager()->countDeaths( - ESM::RefId::stringRefId(select.getName())); + return MWBase::Environment::get().getMechanicsManager()->countDeaths(select.getId()); - case SelectWrapper::Function_Choice: + case ESM::DialogueCondition::Function_Choice: return mChoice; - case SelectWrapper::Function_AiSetting: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: + { + int argument = select.getArgument(); + if (argument < 0 || argument > 3) + { + throw std::runtime_error("AiSetting index is out of range"); + } return mActor.getClass() .getCreatureStats(mActor) - .getAiSetting((MWMechanics::AiSetting)select.getArgument()) + .getAiSetting(static_cast(argument)) .getModified(false); - - case SelectWrapper::Function_PcAttribute: + } + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: { ESM::RefId attribute = ESM::Attribute::indexToRefId(select.getArgument()); return player.getClass().getCreatureStats(player).getAttribute(attribute).getModified(); } - case SelectWrapper::Function_PcSkill: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: { ESM::RefId skill = ESM::Skill::indexToRefId(select.getArgument()); return static_cast(player.getClass().getNpcStats(player).getSkill(skill).getModified()); } - case SelectWrapper::Function_FriendlyHit: + case ESM::DialogueCondition::Function_FriendHit: { int hits = mActor.getClass().getCreatureStats(mActor).getFriendlyHits(); return hits > 4 ? 4 : hits; } - case SelectWrapper::Function_PcLevel: + case ESM::DialogueCondition::Function_PcLevel: return player.getClass().getCreatureStats(player).getLevel(); - case SelectWrapper::Function_PcGender: + case ESM::DialogueCondition::Function_PcGender: return player.get()->mBase->isMale() ? 0 : 1; - case SelectWrapper::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcClothingModifier: { const MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); @@ -423,11 +465,11 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons return value; } - case SelectWrapper::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_PcCrimeLevel: return player.getClass().getNpcStats(player).getBounty(); - case SelectWrapper::Function_RankRequirement: + case ESM::DialogueCondition::Function_RankRequirement: { const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) @@ -449,23 +491,23 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons return result; } - case SelectWrapper::Function_Level: + case ESM::DialogueCondition::Function_Level: return mActor.getClass().getCreatureStats(mActor).getLevel(); - case SelectWrapper::Function_PCReputation: + case ESM::DialogueCondition::Function_PcReputation: return player.getClass().getNpcStats(player).getReputation(); - case SelectWrapper::Function_Weather: + case ESM::DialogueCondition::Function_Weather: return MWBase::Environment::get().getWorld()->getCurrentWeather(); - case SelectWrapper::Function_Reputation: + case ESM::DialogueCondition::Function_Reputation: return mActor.getClass().getNpcStats(mActor).getReputation(); - case SelectWrapper::Function_FactionRankDiff: + case ESM::DialogueCondition::Function_FactionRankDifference: { const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); @@ -477,14 +519,14 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons return rank - npcRank; } - case SelectWrapper::Function_WerewolfKills: + case ESM::DialogueCondition::Function_PcWerewolfKills: return player.getClass().getNpcStats(player).getWerewolfKills(); - case SelectWrapper::Function_RankLow: - case SelectWrapper::Function_RankHigh: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: { - bool low = select.getFunction() == SelectWrapper::Function_RankLow; + bool low = select.getFunction() == ESM::DialogueCondition::Function_FacReactionLowest; const ESM::RefId& factionId = mActor.getClass().getPrimaryFaction(mActor); @@ -507,7 +549,7 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons return value; } - case SelectWrapper::Function_CreatureTargetted: + case ESM::DialogueCondition::Function_CreatureTarget: { MWWorld::Ptr target; @@ -534,53 +576,49 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con switch (select.getFunction()) { - case SelectWrapper::Function_False: + case ESM::DialogueCondition::Function_NotId: - return false; + return mActor.getCellRef().getRefId() != select.getId(); - case SelectWrapper::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: - return !(mActor.getCellRef().getRefId() == ESM::RefId::stringRefId(select.getName())); + return mActor.getClass().getPrimaryFaction(mActor) != select.getId(); - case SelectWrapper::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: - return !(mActor.getClass().getPrimaryFaction(mActor) == ESM::RefId::stringRefId(select.getName())); + return mActor.get()->mBase->mClass != select.getId(); - case SelectWrapper::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: - return !(mActor.get()->mBase->mClass == ESM::RefId::stringRefId(select.getName())); + return mActor.get()->mBase->mRace != select.getId(); - case SelectWrapper::Function_NotRace: - - return !(mActor.get()->mBase->mRace == ESM::RefId::stringRefId(select.getName())); - - case SelectWrapper::Function_NotCell: + case ESM::DialogueCondition::Function_NotCell: { std::string_view actorCell = MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()); - return !Misc::StringUtils::ciStartsWith(actorCell, select.getName()); + return !Misc::StringUtils::ciStartsWith(actorCell, select.getCellName()); } - case SelectWrapper::Function_SameGender: + case ESM::DialogueCondition::Function_SameSex: return (player.get()->mBase->mFlags & ESM::NPC::Female) == (mActor.get()->mBase->mFlags & ESM::NPC::Female); - case SelectWrapper::Function_SameRace: + case ESM::DialogueCondition::Function_SameRace: return mActor.get()->mBase->mRace == player.get()->mBase->mRace; - case SelectWrapper::Function_SameFaction: + case ESM::DialogueCondition::Function_SameFaction: return player.getClass().getNpcStats(player).isInFaction(mActor.getClass().getPrimaryFaction(mActor)); - case SelectWrapper::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcCommonDisease: return player.getClass().getCreatureStats(player).hasCommonDisease(); - case SelectWrapper::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: return player.getClass().getCreatureStats(player).hasBlightDisease(); - case SelectWrapper::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcCorprus: return player.getClass() .getCreatureStats(player) @@ -589,7 +627,7 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con .getMagnitude() != 0; - case SelectWrapper::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcExpelled: { const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); @@ -599,7 +637,7 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con return player.getClass().getNpcStats(player).getExpelled(faction); } - case SelectWrapper::Function_PcVampire: + case ESM::DialogueCondition::Function_PcVampire: return player.getClass() .getCreatureStats(player) @@ -608,27 +646,27 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con .getMagnitude() > 0; - case SelectWrapper::Function_TalkedToPc: + case ESM::DialogueCondition::Function_TalkedToPc: return mTalkedToPlayer; - case SelectWrapper::Function_Alarmed: + case ESM::DialogueCondition::Function_Alarmed: return mActor.getClass().getCreatureStats(mActor).isAlarmed(); - case SelectWrapper::Function_Detected: + case ESM::DialogueCondition::Function_Detected: return MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, mActor); - case SelectWrapper::Function_Attacked: + case ESM::DialogueCondition::Function_Attacked: return mActor.getClass().getCreatureStats(mActor).getAttacked(); - case SelectWrapper::Function_ShouldAttack: + case ESM::DialogueCondition::Function_ShouldAttack: return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, MWMechanics::getPlayer()); - case SelectWrapper::Function_Werewolf: + case ESM::DialogueCondition::Function_Werewolf: return mActor.getClass().getNpcStats(mActor).isWerewolf(); diff --git a/apps/openmw/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index e4d9453c83..28a2e16699 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -250,7 +250,7 @@ namespace MWDialogue void Journal::readRecord(ESM::ESMReader& reader, uint32_t type) { - if (type == ESM::REC_JOUR || type == ESM::REC_JOUR_LEGACY) + if (type == ESM::REC_JOUR) { ESM::JournalEntry record; record.load(reader); diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index 3b784cd59c..2c98eac218 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -87,7 +87,7 @@ namespace MWDialogue // some keywords might be longer variations of other keywords, so we definitely need a list of // candidates the first element in the pair is length of the match, i.e. depth from the first character // on - std::vector> candidates; + std::vector> candidates; while ((j + 1) != end) { @@ -148,11 +148,11 @@ namespace MWDialogue // resolve overlapping keywords while (!matches.empty()) { - int longestKeywordSize = 0; + std::size_t longestKeywordSize = 0; typename std::vector::iterator longestKeyword = matches.begin(); for (typename std::vector::iterator it = matches.begin(); it != matches.end(); ++it) { - int size = it->mEnd - it->mBeg; + std::size_t size = it->mEnd - it->mBeg; if (size > longestKeywordSize) { longestKeywordSize = size; @@ -199,7 +199,7 @@ namespace MWDialogue void seed_impl(std::string_view keyword, value_t value, size_t depth, Entry& entry) { - int ch = Misc::StringUtils::toLower(keyword.at(depth)); + auto ch = Misc::StringUtils::toLower(keyword.at(depth)); typename Entry::childen_t::iterator j = entry.mChildren.find(ch); diff --git a/apps/openmw/mwdialogue/selectwrapper.cpp b/apps/openmw/mwdialogue/selectwrapper.cpp index 94f7f73097..02c9d29b59 100644 --- a/apps/openmw/mwdialogue/selectwrapper.cpp +++ b/apps/openmw/mwdialogue/selectwrapper.cpp @@ -10,431 +10,264 @@ namespace { template - bool selectCompareImp(char comp, T1 value1, T2 value2) + bool selectCompareImp(ESM::DialogueCondition::Comparison comp, T1 value1, T2 value2) { switch (comp) { - case '0': + case ESM::DialogueCondition::Comp_Eq: return value1 == value2; - case '1': + case ESM::DialogueCondition::Comp_Ne: return value1 != value2; - case '2': + case ESM::DialogueCondition::Comp_Gt: return value1 > value2; - case '3': + case ESM::DialogueCondition::Comp_Ge: return value1 >= value2; - case '4': + case ESM::DialogueCondition::Comp_Ls: return value1 < value2; - case '5': + case ESM::DialogueCondition::Comp_Le: return value1 <= value2; + default: + throw std::runtime_error("unknown compare type in dialogue info select"); } - - throw std::runtime_error("unknown compare type in dialogue info select"); } template - bool selectCompareImp(const ESM::DialInfo::SelectStruct& select, T value1) + bool selectCompareImp(const ESM::DialogueCondition& select, T value1) { - if (select.mValue.getType() == ESM::VT_Int) - { - return selectCompareImp(select.mSelectRule[4], value1, select.mValue.getInteger()); - } - else if (select.mValue.getType() == ESM::VT_Float) - { - return selectCompareImp(select.mSelectRule[4], value1, select.mValue.getFloat()); - } - else - throw std::runtime_error("unsupported variable type in dialogue info select"); + return std::visit( + [&](auto value) { return selectCompareImp(select.mComparison, value1, value); }, select.mValue); } } -MWDialogue::SelectWrapper::Function MWDialogue::SelectWrapper::decodeFunction() const -{ - const int index = Misc::StringUtils::toNumeric(mSelect.mSelectRule.substr(2, 2), 0); - - switch (index) - { - case 0: - return Function_RankLow; - case 1: - return Function_RankHigh; - case 2: - return Function_RankRequirement; - case 3: - return Function_Reputation; - case 4: - return Function_HealthPercent; - case 5: - return Function_PCReputation; - case 6: - return Function_PcLevel; - case 7: - return Function_PcHealthPercent; - case 8: - case 9: - return Function_PcDynamicStat; - case 10: - return Function_PcAttribute; - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - case 30: - case 31: - case 32: - case 33: - case 34: - case 35: - case 36: - case 37: - return Function_PcSkill; - case 38: - return Function_PcGender; - case 39: - return Function_PcExpelled; - case 40: - return Function_PcCommonDisease; - case 41: - return Function_PcBlightDisease; - case 42: - return Function_PcClothingModifier; - case 43: - return Function_PcCrimeLevel; - case 44: - return Function_SameGender; - case 45: - return Function_SameRace; - case 46: - return Function_SameFaction; - case 47: - return Function_FactionRankDiff; - case 48: - return Function_Detected; - case 49: - return Function_Alarmed; - case 50: - return Function_Choice; - case 51: - case 52: - case 53: - case 54: - case 55: - case 56: - case 57: - return Function_PcAttribute; - case 58: - return Function_PcCorprus; - case 59: - return Function_Weather; - case 60: - return Function_PcVampire; - case 61: - return Function_Level; - case 62: - return Function_Attacked; - case 63: - return Function_TalkedToPc; - case 64: - return Function_PcDynamicStat; - case 65: - return Function_CreatureTargetted; - case 66: - return Function_FriendlyHit; - case 67: - case 68: - case 69: - case 70: - return Function_AiSetting; - case 71: - return Function_ShouldAttack; - case 72: - return Function_Werewolf; - case 73: - return Function_WerewolfKills; - } - - return Function_False; -} - -MWDialogue::SelectWrapper::SelectWrapper(const ESM::DialInfo::SelectStruct& select) +MWDialogue::SelectWrapper::SelectWrapper(const ESM::DialogueCondition& select) : mSelect(select) { } -MWDialogue::SelectWrapper::Function MWDialogue::SelectWrapper::getFunction() const +ESM::DialogueCondition::Function MWDialogue::SelectWrapper::getFunction() const { - char type = mSelect.mSelectRule[1]; - - switch (type) - { - case '1': - return decodeFunction(); - case '2': - return Function_Global; - case '3': - return Function_Local; - case '4': - return Function_Journal; - case '5': - return Function_Item; - case '6': - return Function_Dead; - case '7': - return Function_NotId; - case '8': - return Function_NotFaction; - case '9': - return Function_NotClass; - case 'A': - return Function_NotRace; - case 'B': - return Function_NotCell; - case 'C': - return Function_NotLocal; - } - - return Function_None; + return mSelect.mFunction; } int MWDialogue::SelectWrapper::getArgument() const { - if (mSelect.mSelectRule[1] != '1') - return 0; - - int index = 0; - - std::istringstream(mSelect.mSelectRule.substr(2, 2)) >> index; - - switch (index) + switch (mSelect.mFunction) { // AI settings - case 67: + case ESM::DialogueCondition::Function_Fight: return 1; - case 68: + case ESM::DialogueCondition::Function_Hello: return 0; - case 69: + case ESM::DialogueCondition::Function_Alarm: return 3; - case 70: + case ESM::DialogueCondition::Function_Flee: return 2; // attributes - case 10: + case ESM::DialogueCondition::Function_PcStrength: return 0; - case 51: + case ESM::DialogueCondition::Function_PcIntelligence: return 1; - case 52: + case ESM::DialogueCondition::Function_PcWillpower: return 2; - case 53: + case ESM::DialogueCondition::Function_PcAgility: return 3; - case 54: + case ESM::DialogueCondition::Function_PcSpeed: return 4; - case 55: + case ESM::DialogueCondition::Function_PcEndurance: return 5; - case 56: + case ESM::DialogueCondition::Function_PcPersonality: return 6; - case 57: + case ESM::DialogueCondition::Function_PcLuck: return 7; // skills - case 11: + case ESM::DialogueCondition::Function_PcBlock: return 0; - case 12: + case ESM::DialogueCondition::Function_PcArmorer: return 1; - case 13: + case ESM::DialogueCondition::Function_PcMediumArmor: return 2; - case 14: + case ESM::DialogueCondition::Function_PcHeavyArmor: return 3; - case 15: + case ESM::DialogueCondition::Function_PcBluntWeapon: return 4; - case 16: + case ESM::DialogueCondition::Function_PcLongBlade: return 5; - case 17: + case ESM::DialogueCondition::Function_PcAxe: return 6; - case 18: + case ESM::DialogueCondition::Function_PcSpear: return 7; - case 19: + case ESM::DialogueCondition::Function_PcAthletics: return 8; - case 20: + case ESM::DialogueCondition::Function_PcEnchant: return 9; - case 21: + case ESM::DialogueCondition::Function_PcDestruction: return 10; - case 22: + case ESM::DialogueCondition::Function_PcAlteration: return 11; - case 23: + case ESM::DialogueCondition::Function_PcIllusion: return 12; - case 24: + case ESM::DialogueCondition::Function_PcConjuration: return 13; - case 25: + case ESM::DialogueCondition::Function_PcMysticism: return 14; - case 26: + case ESM::DialogueCondition::Function_PcRestoration: return 15; - case 27: + case ESM::DialogueCondition::Function_PcAlchemy: return 16; - case 28: + case ESM::DialogueCondition::Function_PcUnarmored: return 17; - case 29: + case ESM::DialogueCondition::Function_PcSecurity: return 18; - case 30: + case ESM::DialogueCondition::Function_PcSneak: return 19; - case 31: + case ESM::DialogueCondition::Function_PcAcrobatics: return 20; - case 32: + case ESM::DialogueCondition::Function_PcLightArmor: return 21; - case 33: + case ESM::DialogueCondition::Function_PcShortBlade: return 22; - case 34: + case ESM::DialogueCondition::Function_PcMarksman: return 23; - case 35: + case ESM::DialogueCondition::Function_PcMerchantile: return 24; - case 36: + case ESM::DialogueCondition::Function_PcSpeechcraft: return 25; - case 37: + case ESM::DialogueCondition::Function_PcHandToHand: return 26; // dynamic stats - case 8: + case ESM::DialogueCondition::Function_PcMagicka: return 1; - case 9: + case ESM::DialogueCondition::Function_PcFatigue: return 2; - case 64: + case ESM::DialogueCondition::Function_PcHealth: + return 0; + default: return 0; } - - return 0; } MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const { - static const Function integerFunctions[] = { - Function_Journal, - Function_Item, - Function_Dead, - Function_Choice, - Function_AiSetting, - Function_PcAttribute, - Function_PcSkill, - Function_FriendlyHit, - Function_PcLevel, - Function_PcGender, - Function_PcClothingModifier, - Function_PcCrimeLevel, - Function_RankRequirement, - Function_Level, - Function_PCReputation, - Function_Weather, - Function_Reputation, - Function_FactionRankDiff, - Function_WerewolfKills, - Function_RankLow, - Function_RankHigh, - Function_CreatureTargetted, - // end marker - Function_None, - }; - - static const Function numericFunctions[] = { - Function_Global, - Function_Local, - Function_NotLocal, - Function_PcDynamicStat, - Function_PcHealthPercent, - Function_HealthPercent, - // end marker - Function_None, - }; - - static const Function booleanFunctions[] = { - Function_False, - Function_SameGender, - Function_SameRace, - Function_SameFaction, - Function_PcCommonDisease, - Function_PcBlightDisease, - Function_PcCorprus, - Function_PcExpelled, - Function_PcVampire, - Function_TalkedToPc, - Function_Alarmed, - Function_Detected, - Function_Attacked, - Function_ShouldAttack, - Function_Werewolf, - // end marker - Function_None, - }; - - static const Function invertedBooleanFunctions[] = { - Function_NotId, - Function_NotFaction, - Function_NotClass, - Function_NotRace, - Function_NotCell, - // end marker - Function_None, - }; - - Function function = getFunction(); - - for (int i = 0; integerFunctions[i] != Function_None; ++i) - if (integerFunctions[i] == function) + switch (mSelect.mFunction) + { + case ESM::DialogueCondition::Function_Journal: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_Choice: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + case ESM::DialogueCondition::Function_FriendHit: + case ESM::DialogueCondition::Function_PcLevel: + case ESM::DialogueCondition::Function_PcGender: + case ESM::DialogueCondition::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_RankRequirement: + case ESM::DialogueCondition::Function_Level: + case ESM::DialogueCondition::Function_PcReputation: + case ESM::DialogueCondition::Function_Weather: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_FactionRankDifference: + case ESM::DialogueCondition::Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: + case ESM::DialogueCondition::Function_CreatureTarget: return Type_Integer; - - for (int i = 0; numericFunctions[i] != Function_None; ++i) - if (numericFunctions[i] == function) + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: + case ESM::DialogueCondition::Function_PcHealth: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: return Type_Numeric; - - for (int i = 0; booleanFunctions[i] != Function_None; ++i) - if (booleanFunctions[i] == function) + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcVampire: + case ESM::DialogueCondition::Function_TalkedToPc: + case ESM::DialogueCondition::Function_Alarmed: + case ESM::DialogueCondition::Function_Detected: + case ESM::DialogueCondition::Function_Attacked: + case ESM::DialogueCondition::Function_ShouldAttack: + case ESM::DialogueCondition::Function_Werewolf: return Type_Boolean; - - for (int i = 0; invertedBooleanFunctions[i] != Function_None; ++i) - if (invertedBooleanFunctions[i] == function) + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: return Type_Inverted; - - return Type_None; + default: + return Type_None; + }; } bool MWDialogue::SelectWrapper::isNpcOnly() const { - static const Function functions[] = { - Function_NotFaction, - Function_NotClass, - Function_NotRace, - Function_SameGender, - Function_SameRace, - Function_SameFaction, - Function_RankRequirement, - Function_Reputation, - Function_FactionRankDiff, - Function_Werewolf, - Function_WerewolfKills, - Function_RankLow, - Function_RankHigh, - // end marker - Function_None, - }; - - Function function = getFunction(); - - for (int i = 0; functions[i] != Function_None; ++i) - if (functions[i] == function) + switch (mSelect.mFunction) + { + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_RankRequirement: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_FactionRankDifference: + case ESM::DialogueCondition::Function_Werewolf: + case ESM::DialogueCondition::Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: return true; - - return false; + default: + return false; + } } bool MWDialogue::SelectWrapper::selectCompare(int value) const @@ -454,5 +287,15 @@ bool MWDialogue::SelectWrapper::selectCompare(bool value) const std::string MWDialogue::SelectWrapper::getName() const { - return Misc::StringUtils::lowerCase(std::string_view(mSelect.mSelectRule).substr(5)); + return Misc::StringUtils::lowerCase(mSelect.mVariable); +} + +std::string_view MWDialogue::SelectWrapper::getCellName() const +{ + return mSelect.mVariable; +} + +ESM::RefId MWDialogue::SelectWrapper::getId() const +{ + return ESM::RefId::stringRefId(mSelect.mVariable); } diff --git a/apps/openmw/mwdialogue/selectwrapper.hpp b/apps/openmw/mwdialogue/selectwrapper.hpp index 0d376d957c..d831b6cea0 100644 --- a/apps/openmw/mwdialogue/selectwrapper.hpp +++ b/apps/openmw/mwdialogue/selectwrapper.hpp @@ -7,62 +7,9 @@ namespace MWDialogue { class SelectWrapper { - const ESM::DialInfo::SelectStruct& mSelect; + const ESM::DialogueCondition& mSelect; public: - enum Function - { - Function_None, - Function_False, - Function_Journal, - Function_Item, - Function_Dead, - Function_NotId, - Function_NotFaction, - Function_NotClass, - Function_NotRace, - Function_NotCell, - Function_NotLocal, - Function_Local, - Function_Global, - Function_SameGender, - Function_SameRace, - Function_SameFaction, - Function_Choice, - Function_PcCommonDisease, - Function_PcBlightDisease, - Function_PcCorprus, - Function_AiSetting, - Function_PcAttribute, - Function_PcSkill, - Function_PcExpelled, - Function_PcVampire, - Function_FriendlyHit, - Function_TalkedToPc, - Function_PcLevel, - Function_PcHealthPercent, - Function_PcDynamicStat, - Function_PcGender, - Function_PcClothingModifier, - Function_PcCrimeLevel, - Function_RankRequirement, - Function_HealthPercent, - Function_Level, - Function_PCReputation, - Function_Weather, - Function_Reputation, - Function_Alarmed, - Function_FactionRankDiff, - Function_Detected, - Function_Attacked, - Function_ShouldAttack, - Function_CreatureTargetted, - Function_Werewolf, - Function_WerewolfKills, - Function_RankLow, - Function_RankHigh - }; - enum Type { Type_None, @@ -72,13 +19,10 @@ namespace MWDialogue Type_Inverted }; - private: - Function decodeFunction() const; - public: - SelectWrapper(const ESM::DialInfo::SelectStruct& select); + SelectWrapper(const ESM::DialogueCondition& select); - Function getFunction() const; + ESM::DialogueCondition::Function getFunction() const; int getArgument() const; @@ -95,6 +39,10 @@ namespace MWDialogue std::string getName() const; ///< Return case-smashed name. + + std::string_view getCellName() const; + + ESM::RefId getId() const; }; } diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index 1eb41a2d86..5a6245fca0 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -6,7 +6,9 @@ #include #include #include +#include +#include #include #include @@ -27,7 +29,6 @@ #include "itemview.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" -#include "ustring.hpp" #include "widgets.hpp" namespace MWGui @@ -77,6 +78,11 @@ namespace MWGui mIngredients[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); + mApparatus[0]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[1]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mCreateButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCreateButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCancelButtonClicked); @@ -141,12 +147,12 @@ namespace MWGui } // remove ingredient slots that have been fully used up - for (int i = 0; i < 4; ++i) + for (size_t i = 0; i < mIngredients.size(); ++i) if (mIngredients[i]->isUserString("ToolTipType")) { MWWorld::Ptr ingred = *mIngredients[i]->getUserData(); - if (ingred.getRefData().getCount() == 0) - removeIngredient(mIngredients[i]); + if (ingred.getCellRef().getCount() == 0) + mAlchemy->removeIngredient(i); } updateFilters(); @@ -158,7 +164,7 @@ namespace MWGui auto const& wm = MWBase::Environment::get().getWindowManager(); std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); - if (mFilterType->getCaption() == toUString(ingredient)) + if (mFilterType->getCaption() == ingredient) mCurrentFilter = FilterType::ByName; else mCurrentFilter = FilterType::ByEffect; @@ -170,17 +176,17 @@ namespace MWGui void AlchemyWindow::switchFilterType(MyGUI::Widget* _sender) { auto const& wm = MWBase::Environment::get().getWindowManager(); - MyGUI::UString ingredient = toUString(wm->getGameSettingString("sIngredients", "Ingredients")); + std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); auto* button = _sender->castType(); if (button->getCaption() == ingredient) { - button->setCaption(toUString(wm->getGameSettingString("sMagicEffects", "Magic Effects"))); + button->setCaption(MyGUI::UString(wm->getGameSettingString("sMagicEffects", "Magic Effects"))); mCurrentFilter = FilterType::ByEffect; } else { - button->setCaption(ingredient); + button->setCaption(MyGUI::UString(ingredient)); mCurrentFilter = FilterType::ByName; } mSortModel->setNameFilter({}); @@ -289,7 +295,85 @@ namespace MWGui void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _sender) { - removeIngredient(_sender); + size_t i = std::distance(mIngredients.begin(), std::find(mIngredients.begin(), mIngredients.end(), _sender)); + mAlchemy->removeIngredient(i); + update(); + } + + void AlchemyWindow::onItemSelected(MWWorld::Ptr item) + { + mItemSelectionDialog->setVisible(false); + + int32_t index = item.get()->mBase->mData.mType; + const auto& widget = mApparatus[index]; + + widget->setItem(item); + + if (item.isEmpty()) + { + widget->clearUserStrings(); + return; + } + + mAlchemy->addApparatus(item); + + widget->setUserString("ToolTipType", "ItemPtr"); + widget->setUserData(MWWorld::Ptr(item)); + + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + update(); + } + + void AlchemyWindow::onItemCancel() + { + mItemSelectionDialog->setVisible(false); + } + + void AlchemyWindow::onApparatusSelected(MyGUI::Widget* _sender) + { + size_t i = std::distance(mApparatus.begin(), std::find(mApparatus.begin(), mApparatus.end(), _sender)); + if (_sender->getUserData()->isEmpty()) // if this apparatus slot is empty + { + std::string title; + switch (i) + { + case ESM::Apparatus::AppaType::MortarPestle: + title = "#{sMortar}"; + break; + case ESM::Apparatus::AppaType::Alembic: + title = "#{sAlembic}"; + break; + case ESM::Apparatus::AppaType::Calcinator: + title = "#{sCalcinator}"; + break; + case ESM::Apparatus::AppaType::Retort: + title = "#{sRetort}"; + break; + default: + title = "#{sApparatus}"; + } + + mItemSelectionDialog = std::make_unique(title); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &AlchemyWindow::onItemSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &AlchemyWindow::onItemCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); + mItemSelectionDialog->getSortModel()->setApparatusTypeFilter(i); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyAlchemyTools); + } + else + { + const auto& widget = mApparatus[i]; + mAlchemy->removeApparatus(i); + + if (widget->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(widget->getChildAt(0)); + + widget->clearUserStrings(); + widget->setItem(MWWorld::Ptr()); + widget->setUserData(MWWorld::Ptr()); + } + update(); } @@ -311,8 +395,10 @@ namespace MWGui { std::string suggestedName = mAlchemy->suggestPotionName(); if (suggestedName != mSuggestedPotionName) + { mNameEdit->setCaptionWithReplacing(suggestedName); - mSuggestedPotionName = suggestedName; + mSuggestedPotionName = std::move(suggestedName); + } mSortModel->clearDragItems(); @@ -329,7 +415,7 @@ namespace MWGui } if (!item.isEmpty()) - mSortModel->addDragItem(item, item.getRefData().getCount()); + mSortModel->addDragItem(item, item.getCellRef().getCount()); if (ingredient->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(ingredient->getChildAt(0)); @@ -344,12 +430,12 @@ namespace MWGui ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(MWWorld::Ptr(item)); - ingredient->setCount(item.getRefData().getCount()); + ingredient->setCount(item.getCellRef().getCount()); } mItemView->update(); - std::set effectIds = mAlchemy->listEffects(); + std::vector effectIds = mAlchemy->listEffects(); Widgets::SpellEffectList list; unsigned int effectIndex = 0; for (const MWMechanics::EffectKey& effectKey : effectIds) @@ -386,15 +472,6 @@ namespace MWGui effectsWidget->setCoord(coord); } - void AlchemyWindow::removeIngredient(MyGUI::Widget* ingredient) - { - for (int i = 0; i < 4; ++i) - if (mIngredients[i] == ingredient) - mAlchemy->removeIngredient(i); - - update(); - } - void AlchemyWindow::addRepeatController(MyGUI::Widget* widget) { MyGUI::ControllerItem* item diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 39ea5ec9b3..82e5c3f583 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -10,6 +10,7 @@ #include #include +#include "itemselection.hpp" #include "windowbase.hpp" #include "../mwmechanics/alchemy.hpp" @@ -44,6 +45,8 @@ namespace MWGui }; FilterType mCurrentFilter; + std::unique_ptr mItemSelectionDialog; + ItemView* mItemView; InventoryItemModel* mModel; SortFilterItemModel* mSortModel; @@ -63,6 +66,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onCreateButtonClicked(MyGUI::Widget* _sender); void onIngredientSelected(MyGUI::Widget* _sender); + void onApparatusSelected(MyGUI::Widget* _sender); void onAccept(MyGUI::EditBox*); void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); @@ -84,7 +88,8 @@ namespace MWGui void onSelectedItem(int index); - void removeIngredient(MyGUI::Widget* ingredient); + void onItemSelected(MWWorld::Ptr item); + void onItemCancel(); void createPotions(int count); diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 617c373b0b..3dfdd17627 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -18,7 +19,6 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" -#include "ustring.hpp" #include "widgets.hpp" namespace @@ -56,7 +56,8 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); - okButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); updateBirths(); @@ -70,10 +71,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void BirthDialog::onOpen() @@ -235,7 +236,7 @@ namespace MWGui { MyGUI::TextBox* label = mSpellArea->createWidget("SandBrightText", coord, MyGUI::Align::Default, "Label"); - label->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + label->setCaption(MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( categories[category].label, {}))); mSpellItems.push_back(label); coord.top += lineHeight; diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 5d9256b20d..1966442513 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -1065,7 +1065,7 @@ namespace MWGui { createActiveFormats(newBook); - mBook = newBook; + mBook = std::move(newBook); setPage(newPage); if (newPage < mBook->mPages.size()) diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index 60a86851fa..ef875a18b9 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -83,7 +84,7 @@ namespace MWGui void BookWindow::setPtr(const MWWorld::Ptr& book) { - if (book.isEmpty() || book.getType() != ESM::REC_BOOK) + if (book.isEmpty() || (book.getType() != ESM::REC_BOOK && book.getType() != ESM::REC_BOOK4)) throw std::runtime_error("Invalid argument in BookWindow::setPtr"); mBook = book; @@ -93,11 +94,16 @@ namespace MWGui clearPages(); mCurrentPage = 0; - MWWorld::LiveCellRef* ref = mBook.get(); + const std::string* text; + if (book.getType() == ESM::REC_BOOK) + text = &book.get()->mBase->mText; + else + text = &book.get()->mBase->mText; + bool shrinkTextAtLastTag = book.getType() == ESM::REC_BOOK; Formatting::BookFormatter formatter; - mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); - formatter.markupToWidget(mRightPage, ref->mBase->mText); + mPages = formatter.markupToWidget(mLeftPage, *text, shrinkTextAtLastTag); + formatter.markupToWidget(mRightPage, *text, shrinkTextAtLastTag); updatePages(); diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 19f7d97176..be2d22ae84 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -39,7 +39,7 @@ namespace { const std::string mText; const Response mResponses[3]; - const std::string mSound; + const VFS::Path::Normalized mSound; }; Step sGenerateClassSteps(int number) @@ -63,17 +63,17 @@ namespace switch (order) { case 0: - return { question, { r0, r1, r2 }, sound }; + return { std::move(question), { std::move(r0), std::move(r1), std::move(r2) }, std::move(sound) }; case 1: - return { question, { r0, r2, r1 }, sound }; + return { std::move(question), { std::move(r0), std::move(r2), std::move(r1) }, std::move(sound) }; case 2: - return { question, { r1, r0, r2 }, sound }; + return { std::move(question), { std::move(r1), std::move(r0), std::move(r2) }, std::move(sound) }; case 3: - return { question, { r1, r2, r0 }, sound }; + return { std::move(question), { std::move(r1), std::move(r2), std::move(r0) }, std::move(sound) }; case 4: - return { question, { r2, r0, r1 }, sound }; + return { std::move(question), { std::move(r2), std::move(r0), std::move(r1) }, std::move(sound) }; default: - return { question, { r2, r1, r0 }, sound }; + return { std::move(question), { std::move(r2), std::move(r1), std::move(r0) }, std::move(sound) }; } } } @@ -333,10 +333,10 @@ namespace MWGui if (!classId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerClass(classId); - const ESM::Class* klass = MWBase::Environment::get().getESMStore()->get().find(classId); - if (klass) + const ESM::Class* pickedClass = MWBase::Environment::get().getESMStore()->get().find(classId); + if (pickedClass) { - mPlayerClass = *klass; + mPlayerClass = *pickedClass; } MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mPickClassDialog)); } @@ -454,30 +454,30 @@ namespace MWGui { if (mCreateClassDialog) { - ESM::Class klass; - klass.mName = mCreateClassDialog->getName(); - klass.mDescription = mCreateClassDialog->getDescription(); - klass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); - klass.mData.mIsPlayable = 0x1; - klass.mRecordFlags = 0; + ESM::Class createdClass; + createdClass.mName = mCreateClassDialog->getName(); + createdClass.mDescription = mCreateClassDialog->getDescription(); + createdClass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); + createdClass.mData.mIsPlayable = 0x1; + createdClass.mRecordFlags = 0; std::vector attributes = mCreateClassDialog->getFavoriteAttributes(); - assert(attributes.size() >= klass.mData.mAttribute.size()); - for (size_t i = 0; i < klass.mData.mAttribute.size(); ++i) - klass.mData.mAttribute[i] = ESM::Attribute::refIdToIndex(attributes[i]); + assert(attributes.size() >= createdClass.mData.mAttribute.size()); + for (size_t i = 0; i < createdClass.mData.mAttribute.size(); ++i) + createdClass.mData.mAttribute[i] = ESM::Attribute::refIdToIndex(attributes[i]); std::vector majorSkills = mCreateClassDialog->getMajorSkills(); std::vector minorSkills = mCreateClassDialog->getMinorSkills(); - assert(majorSkills.size() >= klass.mData.mSkills.size()); - assert(minorSkills.size() >= klass.mData.mSkills.size()); - for (size_t i = 0; i < klass.mData.mSkills.size(); ++i) + assert(majorSkills.size() >= createdClass.mData.mSkills.size()); + assert(minorSkills.size() >= createdClass.mData.mSkills.size()); + for (size_t i = 0; i < createdClass.mData.mSkills.size(); ++i) { - klass.mData.mSkills[i][1] = ESM::Skill::refIdToIndex(majorSkills[i]); - klass.mData.mSkills[i][0] = ESM::Skill::refIdToIndex(minorSkills[i]); + createdClass.mData.mSkills[i][1] = ESM::Skill::refIdToIndex(majorSkills[i]); + createdClass.mData.mSkills[i][0] = ESM::Skill::refIdToIndex(minorSkills[i]); } - MWBase::Environment::get().getMechanicsManager()->setPlayerClass(klass); - mPlayerClass = klass; + MWBase::Environment::get().getMechanicsManager()->setPlayerClass(createdClass); + mPlayerClass = std::move(createdClass); // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); @@ -666,9 +666,10 @@ namespace MWGui MWBase::Environment::get().getMechanicsManager()->setPlayerClass(mGenerateClass); - const ESM::Class* klass = MWBase::Environment::get().getESMStore()->get().find(mGenerateClass); + const ESM::Class* generatedClass + = MWBase::Environment::get().getESMStore()->get().find(mGenerateClass); - mPlayerClass = *klass; + mPlayerClass = *generatedClass; } void CharacterCreation::onGenerateClassBack() diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index f71da8bdf5..839f0f5072 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -20,7 +21,6 @@ #include #include "tooltips.hpp" -#include "ustring.hpp" namespace { @@ -129,10 +129,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void PickClassDialog::onOpen() @@ -248,27 +248,27 @@ namespace MWGui if (mCurrentClassId.empty()) return; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::Class* klass = store.get().search(mCurrentClassId); - if (!klass) + const ESM::Class* currentClass = store.get().search(mCurrentClassId); + if (!currentClass) return; ESM::Class::Specialization specialization - = static_cast(klass->mData.mSpecialization); + = static_cast(currentClass->mData.mSpecialization); std::string specName{ MWBase::Environment::get().getWindowManager()->getGameSettingString( ESM::Class::sGmstSpecializationIds[specialization], ESM::Class::sGmstSpecializationIds[specialization]) }; mSpecializationName->setCaption(specName); ToolTips::createSpecializationToolTip(mSpecializationName, specName, specialization); - mFavoriteAttribute[0]->setAttributeId(ESM::Attribute::indexToRefId(klass->mData.mAttribute[0])); - mFavoriteAttribute[1]->setAttributeId(ESM::Attribute::indexToRefId(klass->mData.mAttribute[1])); + mFavoriteAttribute[0]->setAttributeId(ESM::Attribute::indexToRefId(currentClass->mData.mAttribute[0])); + mFavoriteAttribute[1]->setAttributeId(ESM::Attribute::indexToRefId(currentClass->mData.mAttribute[1])); ToolTips::createAttributeToolTip(mFavoriteAttribute[0], mFavoriteAttribute[0]->getAttributeId()); ToolTips::createAttributeToolTip(mFavoriteAttribute[1], mFavoriteAttribute[1]->getAttributeId()); - for (size_t i = 0; i < klass->mData.mSkills.size(); ++i) + for (size_t i = 0; i < currentClass->mData.mSkills.size(); ++i) { - ESM::RefId minor = ESM::Skill::indexToRefId(klass->mData.mSkills[i][0]); - ESM::RefId major = ESM::Skill::indexToRefId(klass->mData.mSkills[i][1]); + ESM::RefId minor = ESM::Skill::indexToRefId(currentClass->mData.mSkills[i][0]); + ESM::RefId major = ESM::Skill::indexToRefId(currentClass->mData.mSkills[i][1]); mMinorSkill[i]->setSkillId(minor); mMajorSkill[i]->setSkillId(major); ToolTips::createSkillToolTip(mMinorSkill[i], minor); @@ -546,10 +546,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } // widget controls @@ -869,7 +869,7 @@ namespace MWGui getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DescriptionDialog::onOkClicked); okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", {}))); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index ff6cf2d5f7..240198eddc 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -71,7 +71,7 @@ namespace MWGui const ItemStack& item = mSortModel->getItem(index); - // We can't take conjured items from a companion NPC + // We can't take conjured items from a companion actor if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); @@ -119,13 +119,13 @@ namespace MWGui } } - void CompanionWindow::setPtr(const MWWorld::Ptr& npc) + void CompanionWindow::setPtr(const MWWorld::Ptr& actor) { - if (npc.isEmpty() || npc.getType() != ESM::REC_NPC_) + if (actor.isEmpty() || !actor.getClass().isActor()) throw std::runtime_error("Invalid argument in CompanionWindow::setPtr"); - mPtr = npc; + mPtr = actor; updateEncumbranceBar(); - auto model = std::make_unique(npc); + auto model = std::make_unique(actor); mModel = model.get(); auto sortModel = std::make_unique(std::move(model)); mSortModel = sortModel.get(); @@ -133,7 +133,7 @@ namespace MWGui mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); - setTitle(npc.getClass().getName(npc)); + setTitle(actor.getClass().getName(actor)); } void CompanionWindow::onFrame(float dt) diff --git a/apps/openmw/mwgui/companionwindow.hpp b/apps/openmw/mwgui/companionwindow.hpp index c85044b472..97f3a0072e 100644 --- a/apps/openmw/mwgui/companionwindow.hpp +++ b/apps/openmw/mwgui/companionwindow.hpp @@ -26,7 +26,7 @@ namespace MWGui void resetReference() override; - void setPtr(const MWWorld::Ptr& npc) override; + void setPtr(const MWWorld::Ptr& actor) override; void onFrame(float dt) override; void clear() override { resetReference(); } diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index d4553b9664..a188f3c86b 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -20,6 +19,8 @@ #include #include +#include "apps/openmw/mwgui/textcolours.hpp" + #include "../mwscript/extensions.hpp" #include "../mwscript/interpretercontext.hpp" @@ -439,7 +440,7 @@ namespace MWGui // If new search term reset position, otherwise continue from current position if (newSearchTerm != mCurrentSearchTerm) { - mCurrentSearchTerm = newSearchTerm; + mCurrentSearchTerm = std::move(newSearchTerm); mCurrentOccurrenceIndex = std::string::npos; } @@ -770,11 +771,6 @@ namespace MWGui return output.append(matches.front()); } - void Console::onResChange(int width, int height) - { - setCoord(10, 10, width - 10, height / 2); - } - void Console::updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) { if (mPtr == currentPtr) diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 79d18847a4..2b6ecfc8ad 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -47,8 +47,6 @@ namespace MWGui void onOpen() override; - void onResChange(int width, int height) override; - // Print a message to the console, in specified color. void print(const std::string& msg, std::string_view color = MWBase::WindowManager::sConsoleColor_Default); diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index ba4f7156c9..09b66672ba 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -7,6 +7,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -114,7 +115,8 @@ namespace MWGui MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); if (item.mBase.getContainerStore() == &store) throw std::runtime_error("Item to copy needs to be from a different container!"); - return *store.add(item.mBase.getCellRef().getRefId(), count, allowAutoEquip); + MWWorld::ManualRef newRef(*MWBase::Environment::get().getESMStore(), item.mBase, count); + return *store.add(newRef.getPtr(), count, allowAutoEquip); } void ContainerItemModel::removeItem(const ItemStack& item, size_t count) @@ -129,7 +131,7 @@ namespace MWGui { if (stacks(*it, item.mBase)) { - int quantity = it->mRef->mData.getCount(false); + int quantity = it->mRef->mRef.getCount(false); // If this is a restocking quantity, just don't remove it if (quantity < 0 && mTrading) toRemove += quantity; @@ -144,11 +146,11 @@ namespace MWGui { if (stacks(source, item.mBase)) { - int refCount = source.getRefData().getCount(); + int refCount = source.getCellRef().getCount(); if (refCount - toRemove <= 0) MWBase::Environment::get().getWorld()->deleteObject(source); else - source.getRefData().setCount(std::max(0, refCount - toRemove)); + source.getCellRef().setCount(std::max(0, refCount - toRemove)); toRemove -= refCount; if (toRemove <= 0) return; @@ -176,7 +178,7 @@ namespace MWGui if (stacks(*it, itemStack.mBase)) { // we already have an item stack of this kind, add to it - itemStack.mCount += it->getRefData().getCount(); + itemStack.mCount += it->getCellRef().getCount(); found = true; break; } @@ -185,7 +187,7 @@ namespace MWGui if (!found) { // no stack yet, create one - ItemStack newItem(*it, this, it->getRefData().getCount()); + ItemStack newItem(*it, this, it->getCellRef().getCount()); mItems.push_back(newItem); } } @@ -198,7 +200,7 @@ namespace MWGui if (stacks(source, itemStack.mBase)) { // we already have an item stack of this kind, add to it - itemStack.mCount += source.getRefData().getCount(); + itemStack.mCount += source.getCellRef().getCount(); found = true; break; } @@ -207,7 +209,7 @@ namespace MWGui if (!found) { // no stack yet, create one - ItemStack newItem(source, this, source.getRefData().getCount()); + ItemStack newItem(source, this, source.getCellRef().getCount()); mItems.push_back(newItem); } } diff --git a/apps/openmw/mwgui/cursor.cpp b/apps/openmw/mwgui/cursor.cpp index 7c95e2fd11..1b6431f0fb 100644 --- a/apps/openmw/mwgui/cursor.cpp +++ b/apps/openmw/mwgui/cursor.cpp @@ -23,8 +23,8 @@ namespace MWGui MyGUI::xml::ElementEnumerator info = _node->getElementEnumerator(); while (info.next("Property")) { - const std::string& key = info->findAttribute("key"); - const std::string& value = info->findAttribute("value"); + auto key = info->findAttribute("key"); + auto value = info->findAttribute("value"); if (key == "Point") mPoint = MyGUI::IntPoint::parse(value); diff --git a/apps/openmw/mwgui/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp index 5d3948e76d..59f695e7f8 100644 --- a/apps/openmw/mwgui/debugwindow.cpp +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -129,6 +129,7 @@ namespace MWGui static std::mutex sBufferMutex; static int64_t sLogStartIndex; static int64_t sLogEndIndex; + static bool hasPrefix = false; void DebugWindow::startLogRecording() { @@ -170,11 +171,17 @@ namespace MWGui addChar(c); if (c == '#') addChar(c); + if (c == '\n') + hasPrefix = false; } }; for (char c : color) addChar(c); - addShieldedStr(prefix); + if (!hasPrefix) + { + addShieldedStr(prefix); + hasPrefix = true; + } addShieldedStr(msg); if (bufferOverflow) sLogStartIndex = (sLogEndIndex + 1) % bufSize; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 4ab77c3956..6f154bb134 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -22,9 +23,11 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "bookpage.hpp" #include "textcolours.hpp" @@ -193,7 +196,7 @@ namespace MWGui std::string topicName = Misc::StringUtils::lowerCase(windowManager->getTranslationDataStorage().topicStandardForm(link)); - std::string displayName = link; + std::string displayName = std::move(link); while (displayName[displayName.size() - 1] == '*') displayName.erase(displayName.size() - 1, 1); @@ -245,7 +248,7 @@ namespace MWGui i = match.mEnd; } if (i != text.end()) - addTopicLink(typesetter, 0, i - text.begin(), text.size()); + addTopicLink(std::move(typesetter), 0, i - text.begin(), text.size()); } } @@ -337,7 +340,7 @@ namespace MWGui void DialogueWindow::onTradeComplete() { MyGUI::UString message = MyGUI::LanguageManager::getInstance().replaceTags("#{sBarterDialog5}"); - addResponse({}, message.asUTF8()); + addResponse({}, message); } bool DialogueWindow::exit() @@ -361,9 +364,8 @@ namespace MWGui if (mCurrentWindowSize == _sender->getSize()) return; - mTopicsList->adjustSize(); + redrawTopicsList(); updateHistory(); - updateTopicFormat(); mCurrentWindowSize = _sender->getSize(); } @@ -531,6 +533,14 @@ namespace MWGui return true; } + void DialogueWindow::redrawTopicsList() + { + mTopicsList->adjustSize(); + + // The topics list has been regenerated so topic formatting needs to be updated + updateTopicFormat(); + } + void DialogueWindow::updateTopicsPane() { mTopicsList->clear(); @@ -588,11 +598,9 @@ namespace MWGui t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated); mTopicLinks[topicId] = std::move(t); } - mTopicsList->adjustSize(); + redrawTopicsList(); updateHistory(); - // The topics list has been regenerated so topic formatting needs to be updated - updateTopicFormat(); } void DialogueWindow::updateHistory(bool scrollbar) @@ -735,6 +743,15 @@ namespace MWGui bool dispositionVisible = false; if (!mPtr.isEmpty() && mPtr.getClass().isNpc()) { + // If actor was a witness to a crime which was payed off, + // restore original disposition immediately. + MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats(mPtr); + if (npcStats.getCrimeId() != -1 && npcStats.getCrimeDispositionModifier() != 0) + { + if (npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId()) + npcStats.setCrimeDispositionModifier(0); + } + dispositionVisible = true; mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition( @@ -744,21 +761,12 @@ namespace MWGui + std::string("/100")); } - bool dispositionWasVisible = mDispositionBar->getVisible(); - - if (dispositionVisible && !dispositionWasVisible) + if (mDispositionBar->getVisible() != dispositionVisible) { - mDispositionBar->setVisible(true); - int offset = mDispositionBar->getHeight() + 5; + mDispositionBar->setVisible(dispositionVisible); + const int offset = (mDispositionBar->getHeight() + 5) * (dispositionVisible ? 1 : -1); mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0, offset, 0, -offset)); - mTopicsList->adjustSize(); - } - else if (!dispositionVisible && dispositionWasVisible) - { - mDispositionBar->setVisible(false); - int offset = mDispositionBar->getHeight() + 5; - mTopicsList->setCoord(mTopicsList->getCoord() - MyGUI::IntCoord(0, offset, 0, -offset)); - mTopicsList->adjustSize(); + redrawTopicsList(); } } @@ -786,18 +794,34 @@ namespace MWGui if (!Settings::gui().mColorTopicEnable) return; - const MyGUI::Colour& specialColour = Settings::gui().mColorTopicSpecific; - const MyGUI::Colour& oldColour = Settings::gui().mColorTopicExhausted; - for (const std::string& keyword : mKeywords) { int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(keyword)); MyGUI::Button* button = mTopicsList->getItemWidget(keyword); + const auto oldCaption = button->getCaption(); + const MyGUI::IntSize oldSize = button->getSize(); + bool changed = false; if (flag & MWBase::DialogueManager::TopicType::Specific) - button->getSubWidgetText()->setTextColour(specialColour); + { + button->changeWidgetSkin("MW_ListLine_Specific"); + changed = true; + } else if (flag & MWBase::DialogueManager::TopicType::Exhausted) - button->getSubWidgetText()->setTextColour(oldColour); + { + button->changeWidgetSkin("MW_ListLine_Exhausted"); + changed = true; + } + + if (changed) + { + button->setCaption(oldCaption); + button->setTextAlign(MyGUI::Align::Left); + MyGUI::ISubWidgetText* text = button->getSubWidgetText(); + if (text != nullptr) + text->setWordWrap(true); + button->setSize(oldSize); + } } } diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 1b79cadca5..8a8b309401 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -190,6 +190,7 @@ namespace MWGui void updateDisposition(); void restock(); void deleteLater(); + void redrawTopicsList(); bool mIsCompanion; std::list mKeywords; diff --git a/apps/openmw/mwgui/draganddrop.cpp b/apps/openmw/mwgui/draganddrop.cpp index c99e97e37d..0fa2cc4e21 100644 --- a/apps/openmw/mwgui/draganddrop.cpp +++ b/apps/openmw/mwgui/draganddrop.cpp @@ -126,7 +126,7 @@ namespace MWGui void DragAndDrop::onFrame() { - if (mIsOnDragAndDrop && mItem.mBase.getRefData().getCount() == 0) + if (mIsOnDragAndDrop && mItem.mBase.getCellRef().getCount() == 0) finish(); } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index e70599697a..af4a3e8ce3 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -26,7 +27,6 @@ #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" -#include "ustring.hpp" namespace MWGui { @@ -95,7 +95,7 @@ namespace MWGui else { std::string_view name = item.getClass().getName(item); - mName->setCaption(toUString(name)); + mName->setCaption(MyGUI::UString(name)); mItemBox->setItem(item); mItemBox->setUserString("ToolTipType", "ItemPtr"); mItemBox->setUserData(MWWorld::Ptr(item)); @@ -115,23 +115,26 @@ namespace MWGui switch (mEnchanting.getCastStyle()) { case ESM::Enchantment::CastOnce: - mTypeButton->setCaption(toUString( + mTypeButton->setCaption(MyGUI::UString( MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce", "Cast Once"))); setConstantEffect(false); break; case ESM::Enchantment::WhenStrikes: - mTypeButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( - "sItemCastWhenStrikes", "When Strikes"))); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastWhenStrikes", "When Strikes"))); setConstantEffect(false); break; case ESM::Enchantment::WhenUsed: - mTypeButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( - "sItemCastWhenUsed", "When Used"))); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastWhenUsed", "When Used"))); setConstantEffect(false); break; case ESM::Enchantment::ConstantEffect: - mTypeButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString( - "sItemCastConstant", "Cast Constant"))); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastConstant", "Cast Constant"))); setConstantEffect(true); break; } @@ -270,7 +273,7 @@ namespace MWGui void EnchantingDialog::notifyEffectsChanged() { - mEffectList.mList = mEffects; + mEffectList.populate(mEffects); mEnchanting.setEffect(mEffectList); updateLabels(); } @@ -370,7 +373,7 @@ namespace MWGui { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("enchant fail")); MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage34}"); - if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getRefData().getCount()) + if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getCellRef().getCount()) { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 8479379976..b2d9415897 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -23,7 +23,7 @@ namespace MWGui::Formatting { /* BookTextParser */ - BookTextParser::BookTextParser(const std::string& text) + BookTextParser::BookTextParser(const std::string& text, bool shrinkTextAtLastTag) : mIndex(0) , mText(text) , mIgnoreNewlineTags(true) @@ -36,20 +36,25 @@ namespace MWGui::Formatting Misc::StringUtils::replaceAll(mText, "\r", {}); - // vanilla game does not show any text after the last EOL tag. - const std::string lowerText = Misc::StringUtils::lowerCase(mText); - size_t brIndex = lowerText.rfind("
    "); - size_t pIndex = lowerText.rfind("

    "); - mPlainTextEnd = 0; - if (brIndex != pIndex) + if (shrinkTextAtLastTag) { - if (brIndex != std::string::npos && pIndex != std::string::npos) - mPlainTextEnd = std::max(brIndex, pIndex); - else if (brIndex != std::string::npos) - mPlainTextEnd = brIndex; - else - mPlainTextEnd = pIndex; + // vanilla game does not show any text after the last EOL tag. + const std::string lowerText = Misc::StringUtils::lowerCase(mText); + size_t brIndex = lowerText.rfind("
    "); + size_t pIndex = lowerText.rfind("

    "); + mPlainTextEnd = 0; + if (brIndex != pIndex) + { + if (brIndex != std::string::npos && pIndex != std::string::npos) + mPlainTextEnd = std::max(brIndex, pIndex); + else if (brIndex != std::string::npos) + mPlainTextEnd = brIndex; + else + mPlainTextEnd = pIndex; + } } + else + mPlainTextEnd = mText.size(); registerTag("br", Event_BrTag); registerTag("p", Event_PTag); @@ -73,6 +78,17 @@ namespace MWGui::Formatting while (mIndex < mText.size()) { char ch = mText[mIndex]; + if (ch == '[') + { + constexpr std::string_view pageBreakTag = "[pagebreak]\n"; + if (std::string_view(mText.data() + mIndex, mText.size() - mIndex).starts_with(pageBreakTag)) + { + mIndex += pageBreakTag.size(); + flushBuffer(); + mIgnoreNewlineTags = false; + return Event_PageBreak; + } + } if (ch == '<') { const size_t tagStart = mIndex + 1; @@ -98,6 +114,8 @@ namespace MWGui::Formatting } } mIgnoreLineEndings = true; + if (type == Event_PTag && !mAttributes.empty()) + flushBuffer(); } else flushBuffer(); @@ -180,9 +198,9 @@ namespace MWGui::Formatting if (tag.empty()) return; - if (tag[0] == '"') + if (tag[0] == '"' || tag[0] == '\'') { - size_t quoteEndPos = tag.find('"', 1); + size_t quoteEndPos = tag.find(tag[0], 1); if (quoteEndPos == std::string::npos) throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); value = tag.substr(1, quoteEndPos - 1); @@ -203,13 +221,13 @@ namespace MWGui::Formatting } } - mAttributes[key] = value; + mAttributes[key] = std::move(value); } } /* BookFormatter */ - Paginator::Pages BookFormatter::markupToWidget( - MyGUI::Widget* parent, const std::string& markup, const int pageWidth, const int pageHeight) + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget* parent, const std::string& markup, + const int pageWidth, const int pageHeight, bool shrinkTextAtLastTag) { Paginator pag(pageWidth, pageHeight); @@ -225,14 +243,16 @@ namespace MWGui::Formatting MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); paper->setNeedMouseFocus(false); - BookTextParser parser(markup); + BookTextParser parser(markup, shrinkTextAtLastTag); bool brBeforeLastTag = false; bool isPrevImg = false; + bool inlineImageInserted = false; for (;;) { BookTextParser::Events event = parser.next(); - if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag) + if (event == BookTextParser::Event_BrTag + || (event == BookTextParser::Event_PTag && parser.getAttributes().empty())) continue; std::string plainText = parser.getReadyText(); @@ -272,6 +292,12 @@ namespace MWGui::Formatting if (!plainText.empty() || brBeforeLastTag || isPrevImg) { + if (inlineImageInserted) + { + pag.setCurrentTop(pag.getCurrentTop() - mTextStyle.mTextSize); + plainText = " " + plainText; + inlineImageInserted = false; + } TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText); elem.paginate(); } @@ -286,6 +312,10 @@ namespace MWGui::Formatting switch (event) { + case BookTextParser::Event_PageBreak: + pag << Paginator::Page(pag.getStartTop(), pag.getCurrentTop()); + pag.setStartTop(pag.getCurrentTop()); + break; case BookTextParser::Event_ImgTag: { const BookTextParser::Attributes& attr = parser.getAttributes(); @@ -293,22 +323,38 @@ namespace MWGui::Formatting auto srcIt = attr.find("src"); if (srcIt == attr.end()) continue; - auto widthIt = attr.find("width"); - if (widthIt == attr.end()) - continue; - auto heightIt = attr.find("height"); - if (heightIt == attr.end()) - continue; + int width = 0; + if (auto widthIt = attr.find("width"); widthIt != attr.end()) + width = MyGUI::utility::parseInt(widthIt->second); + int height = 0; + if (auto heightIt = attr.find("height"); heightIt != attr.end()) + height = MyGUI::utility::parseInt(heightIt->second); const std::string& src = srcIt->second; - int width = MyGUI::utility::parseInt(widthIt->second); - int height = MyGUI::utility::parseInt(heightIt->second); - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - std::string correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); - bool exists = vfs->exists(correctedSrc); - if (!exists) + std::string correctedSrc; + + constexpr std::string_view imgPrefix = "img://"; + if (src.starts_with(imgPrefix)) + { + correctedSrc = src.substr(imgPrefix.size(), src.size() - imgPrefix.size()); + if (width == 0) + { + width = 50; + inlineImageInserted = true; + } + if (height == 0) + height = 50; + } + else + { + if (width == 0 || height == 0) + continue; + correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); + } + + if (!vfs->exists(correctedSrc)) { Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an tag."; break; @@ -326,6 +372,7 @@ namespace MWGui::Formatting else handleFont(parser.getAttributes()); break; + case BookTextParser::Event_PTag: case BookTextParser::Event_DivTag: handleDiv(parser.getAttributes()); break; @@ -343,9 +390,10 @@ namespace MWGui::Formatting return pag.getPages(); } - Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget* parent, const std::string& markup) + Paginator::Pages BookFormatter::markupToWidget( + MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag) { - return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight()); + return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight(), shrinkTextAtLastTag); } void BookFormatter::resetFontProperties() diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index 421bda6f1d..9a215b200b 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -46,10 +46,11 @@ namespace MWGui Event_PTag, Event_ImgTag, Event_DivTag, - Event_FontTag + Event_FontTag, + Event_PageBreak, }; - BookTextParser(const std::string& text); + BookTextParser(const std::string& text, bool shrinkTextAtLastTag); Events next(); @@ -120,9 +121,9 @@ namespace MWGui class BookFormatter { public: - Paginator::Pages markupToWidget( - MyGUI::Widget* parent, const std::string& markup, const int pageWidth, const int pageHeight); - Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup); + Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, const int pageWidth, + const int pageHeight, bool shrinkTextAtLastTag); + Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag); private: void resetFontProperties(); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index d6eb07e8aa..0a37c93b4f 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -421,18 +421,23 @@ namespace MWGui mSpellBox->setUserString("ToolTipType", "Spell"); mSpellBox->setUserString("Spell", spellId.serialize()); + mSpellBox->setUserData(MyGUI::Any::Null); - // use the icon of the first effect - const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - spell->mEffects.mList.front().mEffectID); - - std::string icon = effect->mIcon; - std::replace(icon.begin(), icon.end(), '/', '\\'); - int slashPos = icon.rfind('\\'); - icon.insert(slashPos + 1, "b_"); - icon = Misc::ResourceHelpers::correctIconPath(icon, MWBase::Environment::get().getResourceSystem()->getVFS()); - - mSpellImage->setSpellIcon(icon); + if (!spell->mEffects.mList.empty()) + { + // use the icon of the first effect + const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( + spell->mEffects.mList.front().mData.mEffectID); + std::string icon = effect->mIcon; + std::replace(icon.begin(), icon.end(), '/', '\\'); + size_t slashPos = icon.rfind('\\'); + icon.insert(slashPos + 1, "b_"); + icon = Misc::ResourceHelpers::correctIconPath( + icon, MWBase::Environment::get().getResourceSystem()->getVFS()); + mSpellImage->setSpellIcon(icon); + } + else + mSpellImage->setSpellIcon({}); } void HUD::setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent) @@ -491,6 +496,7 @@ namespace MWGui mSpellStatus->setProgressPosition(0); mSpellImage->setItem(MWWorld::Ptr()); mSpellBox->clearUserStrings(); + mSpellBox->setUserData(MyGUI::Any::Null); } void HUD::unsetSelectedWeapon() @@ -520,6 +526,7 @@ namespace MWGui mWeapBox->setUserString("ToolTipLayout", "HandToHandToolTip"); mWeapBox->setUserString("Caption_HandToHandText", itemName); mWeapBox->setUserString("ImageTexture_HandToHandImage", icon); + mWeapBox->setUserData(MyGUI::Any::Null); } void HUD::setCrosshairVisible(bool visible) @@ -650,17 +657,31 @@ namespace MWGui updateEnemyHealthBar(); } - void HUD::resetEnemy() + void HUD::clear() { mEnemyActorId = -1; mEnemyHealthTimer = -1; - } - void HUD::clear() - { - unsetSelectedSpell(); - unsetSelectedWeapon(); - resetEnemy(); + mWeaponSpellTimer = 0.f; + mWeaponName = std::string(); + mSpellName = std::string(); + mWeaponSpellBox->setVisible(false); + + mWeapStatus->setProgressRange(100); + mWeapStatus->setProgressPosition(0); + mSpellStatus->setProgressRange(100); + mSpellStatus->setProgressPosition(0); + + mWeapImage->setItem(MWWorld::Ptr()); + mSpellImage->setItem(MWWorld::Ptr()); + + mWeapBox->clearUserStrings(); + mWeapBox->setUserData(MyGUI::Any::Null); + mSpellBox->clearUserStrings(); + mSpellBox->setUserData(MyGUI::Any::Null); + + mActiveCell = nullptr; + mHasALastActiveCell = false; } void HUD::customMarkerCreated(MyGUI::Widget* marker) diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 1dd9cdb521..8dd98628c4 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -58,7 +58,6 @@ namespace MWGui MyGUI::Widget* getEffectBox() { return mEffectBox; } void setEnemy(const MWWorld::Ptr& enemy); - void resetEnemy(); void clear() override; diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index e20637d58c..7464290947 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -8,6 +8,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -57,8 +58,9 @@ namespace MWGui { if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) throw std::runtime_error("Item to copy needs to be from a different container!"); - return *mActor.getClass().getContainerStore(mActor).add( - item.mBase.getCellRef().getRefId(), count, allowAutoEquip); + + MWWorld::ManualRef newRef(*MWBase::Environment::get().getESMStore(), item.mBase, count); + return *mActor.getClass().getContainerStore(mActor).add(newRef.getPtr(), count, allowAutoEquip); } void InventoryItemModel::removeItem(const ItemStack& item, size_t count) @@ -115,7 +117,7 @@ namespace MWGui if (!item.getClass().showsInInventory(item)) continue; - ItemStack newItem(item, this, item.getRefData().getCount()); + ItemStack newItem(item, this, item.getCellRef().getCount()); if (mActor.getClass().hasInventoryStore(mActor)) { diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 9ae9ecf2b0..a773b4635b 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -19,6 +19,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -416,6 +417,8 @@ namespace MWGui void InventoryWindow::onWindowResize(MyGUI::Window* _sender) { + WindowBase::clampWindowCoordinates(_sender); + adjustPanes(); const WindowSettingValues settings = getModeSettings(mGuiMode); @@ -439,6 +442,9 @@ namespace MWGui void InventoryWindow::updateArmorRating() { + if (mPtr.isEmpty()) + return; + mArmorRating->setCaptionWithReplacing( "#{sArmor}: " + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) @@ -555,6 +561,20 @@ namespace MWGui std::unique_ptr action = ptr.getClass().use(ptr, force); action->execute(player); + // Handles partial equipping (final part) + if (mEquippedStackableCount.has_value()) + { + // the count to unequip + int count = ptr.getCellRef().getCount() - mDragAndDrop->mDraggedCount - mEquippedStackableCount.value(); + if (count > 0) + { + MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); + invStore.unequipItemQuantity(ptr, count); + updateItemView(); + } + mEquippedStackableCount.reset(); + } + if (isVisible()) { mItemView->update(); @@ -580,27 +600,21 @@ namespace MWGui } // Handles partial equipping - const std::pair, bool> slots = ptr.getClass().getEquipmentSlots(ptr); + mEquippedStackableCount.reset(); + const auto slots = ptr.getClass().getEquipmentSlots(ptr); if (!slots.first.empty() && slots.second) { - int equippedStackableCount = 0; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator slotIt = invStore.getSlot(slots.first.front()); - // Get the count before useItem() + // Save the currently equipped count before useItem() if (slotIt != invStore.end() && slotIt->getCellRef().getRefId() == ptr.getCellRef().getRefId()) - equippedStackableCount = slotIt->getRefData().getCount(); - - useItem(ptr); - int unequipCount = ptr.getRefData().getCount() - mDragAndDrop->mDraggedCount - equippedStackableCount; - if (unequipCount > 0) - { - invStore.unequipItemQuantity(ptr, unequipCount); - updateItemView(); - } + mEquippedStackableCount = slotIt->getCellRef().getCount(); + else + mEquippedStackableCount = 0; } - else - useItem(ptr); + + MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false); // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 // item @@ -726,7 +740,7 @@ namespace MWGui if (!object.getClass().hasToolTip(object)) return; - int count = object.getRefData().getCount(); + int count = object.getCellRef().getCount(); if (object.getClass().isGold(object)) count *= object.getClass().getValue(object); @@ -738,16 +752,14 @@ namespace MWGui // Player must not be paralyzed, knocked down, or dead to pick up an item. const MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && playerStats.isParalyzed()) || playerStats.getKnockedDown() || playerStats.isDead()) + if (playerStats.isParalyzed() || playerStats.getKnockedDown() || playerStats.isDead()) return; MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count); // add to player inventory // can't use ActionTake here because we need an MWWorld::Ptr to the newly inserted object - MWWorld::Ptr newObject - = *player.getClass().getContainerStore(player).add(object, object.getRefData().getCount()); + MWWorld::Ptr newObject = *player.getClass().getContainerStore(player).add(object, count); // remove from world MWBase::Environment::get().getWorld()->deleteObject(object); @@ -779,8 +791,7 @@ namespace MWGui return; const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; ItemModel::ModelIndex selected = -1; diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index f3d8e3dcd6..9fc77ceec5 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -74,6 +74,7 @@ namespace MWGui DragAndDrop* mDragAndDrop; int mSelectedItem; + std::optional mEquippedStackableCount; MWWorld::Ptr mPtr; diff --git a/apps/openmw/mwgui/itemchargeview.cpp b/apps/openmw/mwgui/itemchargeview.cpp index 92fff6f873..02c3cc182c 100644 --- a/apps/openmw/mwgui/itemchargeview.cpp +++ b/apps/openmw/mwgui/itemchargeview.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -18,7 +19,6 @@ #include "itemmodel.hpp" #include "itemwidget.hpp" -#include "ustring.hpp" namespace MWGui { @@ -128,6 +128,11 @@ namespace MWGui mLines.swap(lines); + std::stable_sort(mLines.begin(), mLines.end(), + [](const MWGui::ItemChargeView::Line& a, const MWGui::ItemChargeView::Line& b) { + return Misc::StringUtils::ciLess(a.mText->getCaption(), b.mText->getCaption()); + }); + layoutWidgets(); } @@ -177,7 +182,7 @@ namespace MWGui void ItemChargeView::updateLine(const ItemChargeView::Line& line) { std::string_view name = line.mItemPtr.getClass().getName(line.mItemPtr); - line.mText->setCaption(toUString(name)); + line.mText->setCaption(MyGUI::UString(name)); line.mCharge->setVisible(false); switch (mDisplayMode) diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 4bdd2399fe..a4cf48fcbe 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -58,7 +58,7 @@ namespace MWGui MWWorld::Ptr ItemModel::moveItem(const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip) { MWWorld::Ptr ret = MWWorld::Ptr(); - if (static_cast(item.mBase.getRefData().getCount()) <= count) + if (static_cast(item.mBase.getCellRef().getCount()) <= count) { // We are moving the full stack ret = otherModel->addItem(item, count, allowAutoEquip); diff --git a/apps/openmw/mwgui/itemselection.hpp b/apps/openmw/mwgui/itemselection.hpp index 78f865bb55..fe87d7e38a 100644 --- a/apps/openmw/mwgui/itemselection.hpp +++ b/apps/openmw/mwgui/itemselection.hpp @@ -32,6 +32,8 @@ namespace MWGui void setCategory(int category); void setFilter(int filter); + SortFilterItemModel* getSortModel() { return mSortModel; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp index 5ee74c6e87..05fff2d40f 100644 --- a/apps/openmw/mwgui/itemwidget.cpp +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -202,7 +202,7 @@ namespace MWGui setIcon(ptr); } - void SpellWidget::setSpellIcon(const std::string& icon) + void SpellWidget::setSpellIcon(std::string_view icon) { if (mFrame && !mCurrentFrame.empty()) { diff --git a/apps/openmw/mwgui/itemwidget.hpp b/apps/openmw/mwgui/itemwidget.hpp index 29b0063203..63837ae92f 100644 --- a/apps/openmw/mwgui/itemwidget.hpp +++ b/apps/openmw/mwgui/itemwidget.hpp @@ -58,7 +58,7 @@ namespace MWGui { MYGUI_RTTI_DERIVED(SpellWidget) public: - void setSpellIcon(const std::string& icon); + void setSpellIcon(std::string_view icon); }; } diff --git a/apps/openmw/mwgui/journalwindow.cpp b/apps/openmw/mwgui/journalwindow.cpp index 0cc5a00831..574c425d3e 100644 --- a/apps/openmw/mwgui/journalwindow.cpp +++ b/apps/openmw/mwgui/journalwindow.cpp @@ -126,7 +126,7 @@ namespace MWGui::BookPage::ClickCallback callback = [this](intptr_t linkId) { notifyTopicClicked(linkId); }; getPage(LeftBookPage)->adviseLinkClicked(callback); - getPage(RightBookPage)->adviseLinkClicked(callback); + getPage(RightBookPage)->adviseLinkClicked(std::move(callback)); getPage(LeftBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); @@ -140,7 +140,7 @@ namespace getPage(LeftTopicIndex)->adviseLinkClicked(callback); getPage(CenterTopicIndex)->adviseLinkClicked(callback); - getPage(RightTopicIndex)->adviseLinkClicked(callback); + getPage(RightTopicIndex)->adviseLinkClicked(std::move(callback)); } adjustButton(PrevPageBTN); @@ -376,7 +376,7 @@ namespace setVisible(PageTwoNum, relPages > 1); getPage(LeftBookPage)->showPage((relPages > 0) ? book : Book(), page + 0); - getPage(RightBookPage)->showPage((relPages > 0) ? book : Book(), page + 1); + getPage(RightBookPage)->showPage((relPages > 0) ? std::move(book) : Book(), page + 1); setText(PageOneNum, page + 1); setText(PageTwoNum, page + 2); diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index a8fb52c95e..9d4971951a 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -183,6 +183,10 @@ namespace MWGui return switchFocus(D_Down, false); case MyGUI::KeyCode::Tab: return switchFocus(MyGUI::InputManager::getInstance().isShiftPressed() ? D_Prev : D_Next, true); + case MyGUI::KeyCode::Period: + return switchFocus(D_Prev, true); + case MyGUI::KeyCode::Slash: + return switchFocus(D_Next, true); case MyGUI::KeyCode::Return: case MyGUI::KeyCode::NumpadEnter: case MyGUI::KeyCode::Space: @@ -242,12 +246,12 @@ namespace MWGui bool forward = (direction == D_Next || direction == D_Right || direction == D_Down); - int index = found - keyFocusList.begin(); + std::ptrdiff_t index{ found - keyFocusList.begin() }; index = forward ? (index + 1) : (index - 1); if (wrap) index = (index + keyFocusList.size()) % keyFocusList.size(); else - index = std::clamp(index, 0, keyFocusList.size() - 1); + index = std::clamp(index, 0, keyFocusList.size() - 1); MyGUI::Widget* next = keyFocusList[index]; int vertdiff = next->getTop() - focus->getTop(); diff --git a/apps/openmw/mwgui/layout.cpp b/apps/openmw/mwgui/layout.cpp index fb0fb5e1c5..8d70bc956b 100644 --- a/apps/openmw/mwgui/layout.cpp +++ b/apps/openmw/mwgui/layout.cpp @@ -3,11 +3,10 @@ #include #include #include +#include #include #include -#include "ustring.hpp" - namespace MWGui { void Layout::initialise(std::string_view _layout) @@ -52,16 +51,15 @@ namespace MWGui { MyGUI::Widget* pt; getWidget(pt, name); - static_cast(pt)->setCaption(toUString(caption)); + static_cast(pt)->setCaption(MyGUI::UString(caption)); } void Layout::setTitle(std::string_view title) { MyGUI::Window* window = static_cast(mMainWidget); - MyGUI::UString uTitle = toUString(title); - if (window->getCaption() != uTitle) - window->setCaptionWithReplacing(uTitle); + if (window->getCaption() != title) + window->setCaptionWithReplacing(MyGUI::UString(title)); } MyGUI::Widget* Layout::getWidget(std::string_view _name) diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index b13fdbeeb9..87f2db55a5 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -21,8 +22,9 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwsound/constants.hpp" + #include "class.hpp" -#include "ustring.hpp" namespace { @@ -164,8 +166,10 @@ namespace MWGui const MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); setClassImage(mClassImage, - ESM::RefId::stringRefId(getLevelupClassImage(pcStats.getSkillIncreasesForSpecialization(0), - pcStats.getSkillIncreasesForSpecialization(1), pcStats.getSkillIncreasesForSpecialization(2)))); + ESM::RefId::stringRefId( + getLevelupClassImage(pcStats.getSkillIncreasesForSpecialization(ESM::Class::Specialization::Combat), + pcStats.getSkillIncreasesForSpecialization(ESM::Class::Specialization::Magic), + pcStats.getSkillIncreasesForSpecialization(ESM::Class::Specialization::Stealth)))); int level = creatureStats.getLevel() + 1; mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + MyGUI::utility::toString(level)); @@ -176,7 +180,7 @@ namespace MWGui if (levelupdescription.empty()) levelupdescription = Fallback::Map::getString("Level_Up_Default"); - mLevelDescription->setCaption(toUString(levelupdescription)); + mLevelDescription->setCaption(MyGUI::UString(levelupdescription)); unsigned int availableAttributes = 0; for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) @@ -214,7 +218,7 @@ namespace MWGui center(); // Play LevelUp Music - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3"); + MWBase::Environment::get().getSoundManager()->streamMusic(MWSound::triumphMusic, MWSound::MusicType::Normal); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index fd7afa2d8a..263e676e15 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -37,7 +39,6 @@ namespace MWGui , mLastRenderTime(0.0) , mLoadingOnTime(0.0) , mImportantLabel(false) - , mVisible(false) , mNestedLoadingCount(0) , mProgress(0) , mShowWallpaper(true) @@ -65,7 +66,8 @@ namespace MWGui != supported_extensions.end(); }; - for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator("Splash/")) + constexpr VFS::Path::NormalizedView splash("splash/"); + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(splash)) { if (isSupportedExtension(Misc::getFileExtension(name))) mSplashScreens.push_back(name); @@ -139,7 +141,7 @@ namespace MWGui osg::BoundingSphere computeBound(const osg::Node&) const override { return osg::BoundingSphere(); } }; - void LoadingScreen::loadingOn(bool visible) + void LoadingScreen::loadingOn() { // Early-out if already on if (mNestedLoadingCount++ > 0 && mMainWidget->getVisible()) @@ -158,17 +160,8 @@ namespace MWGui mOldIcoMax = ico->getMaximumNumOfObjectsToCompilePerFrame(); } - mVisible = visible; - mLoadingBox->setVisible(mVisible); setVisible(true); - if (!mVisible) - { - mShowWallpaper = false; - draw(); - return; - } - mShowWallpaper = MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; if (mShowWallpaper) @@ -183,7 +176,6 @@ namespace MWGui { if (--mNestedLoadingCount > 0) return; - mLoadingBox->setVisible(true); // restore if (mLastRenderTime < mLoadingOnTime) { @@ -191,7 +183,7 @@ namespace MWGui // we may still want to show the label if the caller requested it if (mImportantLabel) { - MWBase::Environment::get().getWindowManager()->messageBox(mLoadingText->getCaption().asUTF8()); + MWBase::Environment::get().getWindowManager()->messageBox(mLoadingText->getCaption()); mImportantLabel = false; } } @@ -324,7 +316,7 @@ namespace MWGui void LoadingScreen::draw() { - if (mVisible && !needToDrawLoadingScreen()) + if (!needToDrawLoadingScreen()) return; if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000 * 1) @@ -340,7 +332,12 @@ namespace MWGui MWBase::Environment::get().getInputManager()->update(0, true, true); - mResourceSystem->reportStats(mViewer->getFrameStamp()->getFrameNumber(), mViewer->getViewerStats()); + osg::Stats* const stats = mViewer->getViewerStats(); + const unsigned frameNumber = mViewer->getFrameStamp()->getFrameNumber(); + + stats->setAttribute(frameNumber, "Loading", 1); + + mResourceSystem->reportStats(frameNumber, stats); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { ico->setMinimumTimeAvailableForGLCompileAndDeletePerFrame(1.f / getTargetFrameRate()); diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 2cd3f73576..35de331ee0 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -38,7 +38,7 @@ namespace MWGui /// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details void setLabel(const std::string& label, bool important) override; - void loadingOn(bool visible = true) override; + void loadingOn() override; void loadingOff() override; void setProgressRange(size_t range) override; void setProgress(size_t value) override; @@ -66,7 +66,6 @@ namespace MWGui bool mImportantLabel; - bool mVisible; int mNestedLoadingCount; size_t mProgress; diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index 37e835f1a4..da747dd7a2 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -18,10 +20,61 @@ #include "backgroundimage.hpp" #include "confirmationdialog.hpp" #include "savegamedialog.hpp" +#include "settingswindow.hpp" #include "videowidget.hpp" namespace MWGui { + void MenuVideo::run() + { + Misc::FrameRateLimiter frameRateLimiter + = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); + while (mRunning) + { + // If finished playing, start again + if (!mVideo->update()) + mVideo->playVideo("video\\menu_background.bik"); + frameRateLimiter.limit(); + } + } + + MenuVideo::MenuVideo(const VFS::Manager* vfs) + : mRunning(true) + { + // Use black background to correct aspect ratio + mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal( + "ImageBox", 0, 0, 1, 1, MyGUI::Align::Default, "MainMenuBackground"); + mVideoBackground->setImageTexture("black"); + + mVideo = mVideoBackground->createWidget( + "ImageBox", 0, 0, 1, 1, MyGUI::Align::Stretch, "MainMenuBackground"); + mVideo->setVFS(vfs); + + mVideo->playVideo("video\\menu_background.bik"); + mThread = std::thread([this] { run(); }); + } + + void MenuVideo::resize(int screenWidth, int screenHeight) + { + const bool stretch = Settings::gui().mStretchMenuBackground; + mVideoBackground->setSize(screenWidth, screenHeight); + mVideo->autoResize(stretch); + mVideo->setVisible(true); + } + + MenuVideo::~MenuVideo() + { + mRunning = false; + mThread.join(); + try + { + MyGUI::Gui::getInstance().destroyWidget(mVideoBackground); + } + catch (const MyGUI::Exception& e) + { + Log(Debug::Error) << "Error in the destructor: " << e.what(); + } + } MainMenu::MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription) : WindowBase("openmw_mainmenu.layout") @@ -30,13 +83,13 @@ namespace MWGui , mVFS(vfs) , mButtonBox(nullptr) , mBackground(nullptr) - , mVideoBackground(nullptr) - , mVideo(nullptr) { getWidget(mVersionText, "VersionText"); mVersionText->setCaption(versionDescription); - mHasAnimatedMenu = mVFS->exists("video/menu_background.bik"); + constexpr VFS::Path::NormalizedView menuBackgroundVideo("video/menu_background.bik"); + + mHasAnimatedMenu = mVFS->exists(menuBackgroundVideo); updateMenu(); } @@ -47,6 +100,8 @@ namespace MWGui mHeight = h; updateMenu(); + if (mVideo) + mVideo->resize(w, h); } void MainMenu::setVisible(bool visible) @@ -96,8 +151,6 @@ namespace MWGui { winMgr->removeGuiMode(GM_MainMenu); } - else if (name == "options") - winMgr->pushGuiMode(GM_Settings); else if (name == "credits") winMgr->playVideo("mw_credits.bik", true); else if (name == "exitgame") @@ -126,26 +179,25 @@ namespace MWGui dialog->eventCancelClicked.clear(); } } - - else + else if (name == "loadgame" || name == "savegame") { if (!mSaveGameDialog) mSaveGameDialog = std::make_unique(); - if (name == "loadgame") - mSaveGameDialog->setLoadOrSave(true); - else if (name == "savegame") - mSaveGameDialog->setLoadOrSave(false); + mSaveGameDialog->setLoadOrSave(name == "loadgame"); mSaveGameDialog->setVisible(true); } + + if (winMgr->isSettingsWindowVisible() || name == "options") + { + winMgr->toggleSettingsWindow(); + } } void MainMenu::showBackground(bool show) { if (mVideo && !show) { - MyGUI::Gui::getInstance().destroyWidget(mVideoBackground); - mVideoBackground = nullptr; - mVideo = nullptr; + mVideo.reset(); } if (mBackground && !show) { @@ -161,27 +213,12 @@ namespace MWGui if (mHasAnimatedMenu) { if (!mVideo) - { - // Use black background to correct aspect ratio - mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal( - "ImageBox", 0, 0, 1, 1, MyGUI::Align::Default, "MainMenuBackground"); - mVideoBackground->setImageTexture("black"); + mVideo.emplace(mVFS); - mVideo = mVideoBackground->createWidget( - "ImageBox", 0, 0, 1, 1, MyGUI::Align::Stretch, "MainMenuBackground"); - mVideo->setVFS(mVFS); - - mVideo->playVideo("video\\menu_background.bik"); - } - - MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + const auto& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); int screenWidth = viewSize.width; int screenHeight = viewSize.height; - mVideoBackground->setSize(screenWidth, screenHeight); - - mVideo->autoResize(stretch); - - mVideo->setVisible(true); + mVideo->resize(screenWidth, screenHeight); } else { @@ -195,20 +232,14 @@ namespace MWGui } } - void MainMenu::onFrame(float dt) - { - if (mVideo) - { - if (!mVideo->update()) - { - // If finished playing, start again - mVideo->playVideo("video\\menu_background.bik"); - } - } - } - bool MainMenu::exit() { + if (MWBase::Environment::get().getWindowManager()->isSettingsWindowVisible()) + { + MWBase::Environment::get().getWindowManager()->toggleSettingsWindow(); + return false; + } + return MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Running; } diff --git a/apps/openmw/mwgui/mainmenu.hpp b/apps/openmw/mwgui/mainmenu.hpp index ee6a5cdb16..06a8c945c1 100644 --- a/apps/openmw/mwgui/mainmenu.hpp +++ b/apps/openmw/mwgui/mainmenu.hpp @@ -2,6 +2,8 @@ #define OPENMW_GAME_MWGUI_MAINMENU_H #include +#include +#include #include "savegamedialog.hpp" #include "windowbase.hpp" @@ -21,6 +23,20 @@ namespace MWGui class BackgroundImage; class VideoWidget; + class MenuVideo + { + MyGUI::ImageBox* mVideoBackground; + VideoWidget* mVideo; + std::thread mThread; + bool mRunning; + + void run(); + + public: + MenuVideo(const VFS::Manager* vfs); + void resize(int w, int h); + ~MenuVideo(); + }; class MainMenu : public WindowBase { @@ -36,8 +52,6 @@ namespace MWGui void setVisible(bool visible) override; - void onFrame(float dt) override; - bool exit() override; private: @@ -48,8 +62,7 @@ namespace MWGui BackgroundImage* mBackground; - MyGUI::ImageBox* mVideoBackground; - VideoWidget* mVideo; // For animated main menus + std::optional mVideo; // For animated main menus std::map> mButtons; diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 088024ac63..bf4bd7644c 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -82,19 +82,31 @@ namespace MyGUI::IntRect createRect(const MyGUI::IntPoint& center, int radius) { - return { center.left - radius, center.top + radius, center.left + radius, center.top - radius }; + return { center.left - radius, center.top - radius, center.left + radius, center.top + radius }; } int getLocalViewingDistance() { if (!Settings::map().mAllowZooming) return Constants::CellGridRadius; - if (!Settings::Manager::getBool("distant terrain", "Terrain")) + if (!Settings::terrain().mDistantTerrain) return Constants::CellGridRadius; const int viewingDistanceInCells = Settings::camera().mViewingDistance / Constants::CellSizeInUnits; return std::clamp( viewingDistanceInCells, Constants::CellGridRadius, Settings::map().mMaxLocalViewingDistance.get()); } + + ESM::RefId getCellIdInWorldSpace(const MWWorld::Cell& cell, int x, int y) + { + if (cell.isExterior()) + return ESM::Cell::generateIdForCell(true, {}, x, y); + return cell.getId(); + } + + void setCanvasSize(MyGUI::ScrollView* scrollView, const MyGUI::IntRect& grid, int widgetSize) + { + scrollView->setCanvasSize(widgetSize * (grid.width() + 1), widgetSize * (grid.height() + 1)); + } } namespace MWGui @@ -170,21 +182,8 @@ namespace MWGui LocalMapBase::LocalMapBase( CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled) : mLocalMapRender(localMapRender) - , mCurX(0) - , mCurY(0) - , mInterior(false) - , mLocalMap(nullptr) - , mCompass(nullptr) - , mChanged(true) - , mFogOfWarToggled(true) , mFogOfWarEnabled(fogOfWarEnabled) - , mNumCells(1) - , mCellDistance(0) , mCustomMarkers(markers) - , mMarkerUpdateTimer(0.0f) - , mLastDirectionX(0.0f) - , mLastDirectionY(0.0f) - , mNeedDoorMarkersUpdate(false) { mCustomMarkers.eventMarkersChanged += MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } @@ -194,47 +193,40 @@ namespace MWGui mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } + MWGui::LocalMapBase::MapEntry& LocalMapBase::addMapEntry() + { + const int mapWidgetSize = Settings::map().mLocalMapWidgetSize; + MyGUI::ImageBox* map = mLocalMap->createWidget( + "ImageBox", MyGUI::IntCoord(0, 0, mapWidgetSize, mapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); + map->setDepth(Local_MapLayer); + + MyGUI::ImageBox* fog = mLocalMap->createWidget( + "ImageBox", MyGUI::IntCoord(0, 0, mapWidgetSize, mapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); + fog->setDepth(Local_FogLayer); + fog->setColour(MyGUI::Colour(0, 0, 0)); + + map->setNeedMouseFocus(false); + fog->setNeedMouseFocus(false); + + return mMaps.emplace_back(map, fog); + } + void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance) { mLocalMap = widget; mCompass = compass; - mCellDistance = cellDistance; - mNumCells = mCellDistance * 2 + 1; + mGrid = createRect({ 0, 0 }, cellDistance); + mExtCellDistance = cellDistance; const int mapWidgetSize = Settings::map().mLocalMapWidgetSize; - - mLocalMap->setCanvasSize(mapWidgetSize * mNumCells, mapWidgetSize * mNumCells); + setCanvasSize(mLocalMap, mGrid, mapWidgetSize); mCompass->setDepth(Local_CompassLayer); mCompass->setNeedMouseFocus(false); - for (int mx = 0; mx < mNumCells; ++mx) - { - for (int my = 0; my < mNumCells; ++my) - { - MyGUI::ImageBox* map = mLocalMap->createWidget("ImageBox", - MyGUI::IntCoord(mx * mapWidgetSize, my * mapWidgetSize, mapWidgetSize, mapWidgetSize), - MyGUI::Align::Top | MyGUI::Align::Left); - map->setDepth(Local_MapLayer); - - MyGUI::ImageBox* fog = mLocalMap->createWidget("ImageBox", - MyGUI::IntCoord(mx * mapWidgetSize, my * mapWidgetSize, mapWidgetSize, mapWidgetSize), - MyGUI::Align::Top | MyGUI::Align::Left); - fog->setDepth(Local_FogLayer); - fog->setColour(MyGUI::Colour(0, 0, 0)); - - map->setNeedMouseFocus(false); - fog->setNeedMouseFocus(false); - - mMaps.emplace_back(map, fog); - } - } - } - - void LocalMapBase::setCellPrefix(const std::string& prefix) - { - mPrefix = prefix; - mChanged = true; + int numCells = (mGrid.width() + 1) * (mGrid.height() + 1); + for (int i = 0; i < numCells; ++i) + addMapEntry(); } bool LocalMapBase::toggleFogOfWar() @@ -262,8 +254,8 @@ namespace MWGui { // normalized cell coordinates auto mapWidgetSize = getWidgetSize(); - return MyGUI::IntPoint(std::round(nX * mapWidgetSize + (mCellDistance + (cellX - mCurX)) * mapWidgetSize), - std::round(nY * mapWidgetSize + (mCellDistance - (cellY - mCurY)) * mapWidgetSize)); + return MyGUI::IntPoint(std::round((nX + cellX - mGrid.left) * mapWidgetSize), + std::round((nY - cellY + mGrid.bottom) * mapWidgetSize)); } MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const @@ -272,7 +264,7 @@ namespace MWGui // normalized cell coordinates float nX, nY; - if (!mInterior) + if (mActiveCell->isExterior()) { ESM::ExteriorCellLocation cellPos = ESM::positionToExteriorCellLocation(worldX, worldY); cellIndex.x() = cellPos.mX; @@ -300,11 +292,9 @@ namespace MWGui return MyGUI::IntCoord(position.left - halfMarkerSize, position.top - halfMarkerSize, markerSize, markerSize); } - MyGUI::Widget* LocalMapBase::createDoorMarker( - const std::string& name, const MyGUI::VectorString& notes, float x, float y) const + MyGUI::Widget* LocalMapBase::createDoorMarker(const std::string& name, float x, float y) const { MarkerUserData data(mLocalMapRender); - data.notes = notes; data.caption = name; MarkerWidget* markerWidget = mLocalMap->createWidget( "MarkerButton", getMarkerCoordinates(x, y, data, 8), MyGUI::Align::Default); @@ -338,7 +328,7 @@ namespace MWGui std::vector& LocalMapBase::currentDoorMarkersWidgets() { - return mInterior ? mInteriorDoorMarkerWidgets : mExteriorDoorMarkerWidgets; + return mActiveCell->isExterior() ? mExteriorDoorMarkerWidgets : mInteriorDoorMarkerWidgets; } void LocalMapBase::updateCustomMarkers() @@ -346,57 +336,63 @@ namespace MWGui for (MyGUI::Widget* widget : mCustomMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mCustomMarkerWidgets.clear(); - - for (int dX = -mCellDistance; dX <= mCellDistance; ++dX) - { - for (int dY = -mCellDistance; dY <= mCellDistance; ++dY) + if (!mActiveCell) + return; + auto updateMarkers = [this](CustomMarkerCollection::RangeType markers) { + for (auto it = markers.first; it != markers.second; ++it) { - ESM::RefId cellRefId = ESM::Cell::generateIdForCell(!mInterior, mPrefix, mCurX + dX, mCurY + dY); - - CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellRefId); - for (CustomMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; - ++it) + const ESM::CustomMarker& marker = it->second; + MarkerUserData markerPos(mLocalMapRender); + MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", + getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16), MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); + markerWidget->setUserString("ToolTipType", "Layout"); + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); + markerWidget->setNormalColour(MyGUI::Colour(0.6f, 0.6f, 0.6f)); + markerWidget->setHoverColour(MyGUI::Colour(1.0f, 1.0f, 1.0f)); + markerWidget->setUserData(marker); + markerWidget->setNeedMouseFocus(true); + customMarkerCreated(markerWidget); + mCustomMarkerWidgets.push_back(markerWidget); + } + }; + if (mActiveCell->isExterior()) + { + for (int x = mGrid.left; x <= mGrid.right; ++x) + { + for (int y = mGrid.top; y <= mGrid.bottom; ++y) { - const ESM::CustomMarker& marker = it->second; - - MarkerUserData markerPos(mLocalMapRender); - MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", - getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16), MyGUI::Align::Default); - markerWidget->setDepth(Local_MarkerAboveFogLayer); - markerWidget->setUserString("ToolTipType", "Layout"); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); - markerWidget->setNormalColour(MyGUI::Colour(0.6f, 0.6f, 0.6f)); - markerWidget->setHoverColour(MyGUI::Colour(1.0f, 1.0f, 1.0f)); - markerWidget->setUserData(marker); - markerWidget->setNeedMouseFocus(true); - customMarkerCreated(markerWidget); - mCustomMarkerWidgets.push_back(markerWidget); + ESM::RefId cellRefId = getCellIdInWorldSpace(*mActiveCell, x, y); + updateMarkers(mCustomMarkers.getMarkers(cellRefId)); } } } + else + updateMarkers(mCustomMarkers.getMarkers(mActiveCell->getId())); redraw(); } - void LocalMapBase::setActiveCell(const int x, const int y, bool interior) + void LocalMapBase::setActiveCell(const MWWorld::Cell& cell) { - if (x == mCurX && y == mCurY && mInterior == interior && !mChanged) + if (&cell == mActiveCell) return; // don't do anything if we're still in the same cell - if (!interior && !(x == mCurX && y == mCurY)) - { - const MyGUI::IntRect intersection - = { std::max(x, mCurX) - mCellDistance, std::max(y, mCurY) - mCellDistance, - std::min(x, mCurX) + mCellDistance, std::min(y, mCurY) + mCellDistance }; + const int x = cell.getGridX(); + const int y = cell.getGridY(); + MyGUI::IntSize oldSize{ mGrid.width(), mGrid.height() }; + + if (cell.isExterior()) + { + mGrid = createRect({ x, y }, mExtCellDistance); const MyGUI::IntRect activeGrid = createRect({ x, y }, Constants::CellGridRadius); - const MyGUI::IntRect currentView = createRect({ x, y }, mCellDistance); mExteriorDoorMarkerWidgets.clear(); for (auto& [coord, doors] : mExteriorDoorsByCell) { - if (!mHasALastActiveCell || !currentView.inside({ coord.first, coord.second }) + if (!mHasALastActiveCell || !mGrid.inside({ coord.first, coord.second }) || activeGrid.inside({ coord.first, coord.second })) { mDoorMarkersToRecycle.insert(mDoorMarkersToRecycle.end(), doors.begin(), doors.end()); @@ -409,32 +405,54 @@ namespace MWGui for (auto& widget : mDoorMarkersToRecycle) widget->setVisible(false); - for (auto const& cell : mMaps) + if (mHasALastActiveCell) { - if (mHasALastActiveCell && !intersection.inside({ cell.mCellX, cell.mCellY })) - mLocalMapRender->removeExteriorCell(cell.mCellX, cell.mCellY); + for (const auto& entry : mMaps) + { + if (!mGrid.inside({ entry.mCellX, entry.mCellY })) + mLocalMapRender->removeExteriorCell(entry.mCellX, entry.mCellY); + } } } + else + mGrid = mLocalMapRender->getInteriorGrid(); - mCurX = x; - mCurY = y; - mInterior = interior; - mChanged = false; + mActiveCell = &cell; - for (int mx = 0; mx < mNumCells; ++mx) + constexpr auto resetEntry = [](MapEntry& entry, bool visible, const MyGUI::IntPoint* position) { + entry.mMapWidget->setVisible(visible); + entry.mFogWidget->setVisible(visible); + if (position) + { + entry.mMapWidget->setPosition(*position); + entry.mFogWidget->setPosition(*position); + } + entry.mMapWidget->setRenderItemTexture(nullptr); + entry.mFogWidget->setRenderItemTexture(nullptr); + entry.mMapTexture.reset(); + entry.mFogTexture.reset(); + }; + + std::size_t usedEntries = 0; + for (int cx = mGrid.left; cx <= mGrid.right; ++cx) { - for (int my = 0; my < mNumCells; ++my) + for (int cy = mGrid.top; cy <= mGrid.bottom; ++cy) { - MapEntry& entry = mMaps[my + mNumCells * mx]; - entry.mMapWidget->setRenderItemTexture(nullptr); - entry.mFogWidget->setRenderItemTexture(nullptr); - entry.mMapTexture.reset(); - entry.mFogTexture.reset(); - - entry.mCellX = x + (mx - mCellDistance); - entry.mCellY = y - (my - mCellDistance); + MapEntry& entry = usedEntries < mMaps.size() ? mMaps[usedEntries] : addMapEntry(); + entry.mCellX = cx; + entry.mCellY = cy; + MyGUI::IntPoint position = getPosition(cx, cy, 0, 0); + resetEntry(entry, true, &position); + ++usedEntries; } } + for (std::size_t i = usedEntries; i < mMaps.size(); ++i) + { + resetEntry(mMaps[i], false, nullptr); + } + + if (oldSize != MyGUI::IntSize{ mGrid.width(), mGrid.height() }) + setCanvasSize(mLocalMap, mGrid, getWidgetSize()); // Delay the door markers update until scripts have been given a chance to run. // If we don't do this, door markers that should be disabled will still appear on the map. @@ -443,7 +461,7 @@ namespace MWGui for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) widget->setCoord(getMarkerCoordinates(widget, 8)); - if (!mInterior) + if (mActiveCell->isExterior()) mHasALastActiveCell = true; updateMagicMarkers(); @@ -509,7 +527,7 @@ namespace MWGui if (markers.empty()) return; - std::string markerTexture; + std::string_view markerTexture; if (type == MWBase::World::Detect_Creature) { markerTexture = "textures\\detect_animal_icon.dds"; @@ -561,15 +579,7 @@ namespace MWGui { MyGUI::IntRect coord = widget->getAbsoluteRect(); MyGUI::IntRect croppedCoord = cropTo->getAbsoluteRect(); - if (coord.left < croppedCoord.left && coord.right < croppedCoord.left) - return true; - if (coord.left > croppedCoord.right && coord.right > croppedCoord.right) - return true; - if (coord.top < croppedCoord.top && coord.bottom < croppedCoord.top) - return true; - if (coord.top > croppedCoord.bottom && coord.bottom > croppedCoord.bottom) - return true; - return false; + return !coord.intersect(croppedCoord); } void LocalMapBase::updateRequiredMaps() @@ -582,7 +592,7 @@ namespace MWGui if (!entry.mMapTexture) { - if (!mInterior) + if (mActiveCell->isExterior()) requestMapRender(&MWBase::Environment::get().getWorldModel()->getExterior( ESM::ExteriorCellLocation(entry.mCellX, entry.mCellY, ESM::Cell::sDefaultWorldspaceId))); @@ -628,12 +638,12 @@ namespace MWGui mDoorMarkersToRecycle.end(), mInteriorDoorMarkerWidgets.begin(), mInteriorDoorMarkerWidgets.end()); mInteriorDoorMarkerWidgets.clear(); - if (mInterior) + if (!mActiveCell->isExterior()) { for (MyGUI::Widget* widget : mExteriorDoorMarkerWidgets) widget->setVisible(false); - MWWorld::CellStore& cell = worldModel->getInterior(mPrefix); + MWWorld::CellStore& cell = worldModel->getInterior(mActiveCell->getNameId()); world->getDoorMarkers(cell, doors); } else @@ -662,8 +672,9 @@ namespace MWGui MarkerUserData* data; if (mDoorMarkersToRecycle.empty()) { - markerWidget = createDoorMarker(marker.name, destNotes, marker.x, marker.y); + markerWidget = createDoorMarker(marker.name, marker.x, marker.y); data = markerWidget->getUserData(); + data->notes = std::move(destNotes); doorMarkerCreated(markerWidget); } else @@ -672,14 +683,14 @@ namespace MWGui mDoorMarkersToRecycle.pop_back(); data = markerWidget->getUserData(); - data->notes = destNotes; + data->notes = std::move(destNotes); data->caption = marker.name; markerWidget->setCoord(getMarkerCoordinates(marker.x, marker.y, *data, 8)); markerWidget->setVisible(true); } currentDoorMarkersWidgets().push_back(markerWidget); - if (!mInterior) + if (mActiveCell->isExterior()) mExteriorDoorsByCell[{ data->cellX, data->cellY }].push_back(markerWidget); } @@ -702,8 +713,7 @@ namespace MWGui MWWorld::CellStore* markedCell = nullptr; ESM::Position markedPosition; MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell && markedCell->isExterior() == !mInterior - && (!mInterior || Misc::StringUtils::ciEqual(markedCell->getCell()->getNameId(), mPrefix))) + if (markedCell && markedCell->getCell()->getWorldSpace() == mActiveCell->getWorldSpace()) { MarkerUserData markerPos(mLocalMapRender); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", @@ -722,7 +732,7 @@ namespace MWGui void LocalMapBase::updateLocalMap() { auto mapWidgetSize = getWidgetSize(); - mLocalMap->setCanvasSize(mapWidgetSize * mNumCells, mapWidgetSize * mNumCells); + setCanvasSize(mLocalMap, mGrid, getWidgetSize()); const auto size = MyGUI::IntSize(std::ceil(mapWidgetSize), std::ceil(mapWidgetSize)); for (auto& entry : mMaps) @@ -867,15 +877,13 @@ namespace MWGui MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); auto mapWidgetSize = getWidgetSize(); - int x = int(widgetPos.left / float(mapWidgetSize)) - mCellDistance; - int y = (int(widgetPos.top / float(mapWidgetSize)) - mCellDistance) * -1; + int x = int(widgetPos.left / float(mapWidgetSize)) + mGrid.left; + int y = mGrid.bottom - int(widgetPos.top / float(mapWidgetSize)); float nX = widgetPos.left / float(mapWidgetSize) - int(widgetPos.left / float(mapWidgetSize)); float nY = widgetPos.top / float(mapWidgetSize) - int(widgetPos.top / float(mapWidgetSize)); - x += mCurX; - y += mCurY; osg::Vec2f worldPos; - if (mInterior) + if (!mActiveCell->isExterior()) { worldPos = mLocalMapRender->interiorMapToWorldPosition(nX, nY, x, y); } @@ -887,7 +895,7 @@ namespace MWGui mEditingMarker.mWorldX = worldPos.x(); mEditingMarker.mWorldY = worldPos.y(); - ESM::RefId clickedId = ESM::Cell::generateIdForCell(!mInterior, LocalMapBase::mPrefix, x, y); + ESM::RefId clickedId = getCellIdInWorldSpace(*mActiveCell, x, y); mEditingMarker.mCell = clickedId; @@ -902,11 +910,11 @@ namespace MWGui const bool zoomOut = rel < 0; const bool zoomIn = !zoomOut; const double speedDiff = zoomOut ? 1.0 / speed : speed; - const float localMapSizeInUnits = localWidgetSize * mNumCells; - const float currentMinLocalMapZoom = std::max({ (float(Settings::map().mGlobalMapCellSize) * 4.f) - / float(localWidgetSize), - float(mLocalMap->getWidth()) / localMapSizeInUnits, float(mLocalMap->getHeight()) / localMapSizeInUnits }); + const float currentMinLocalMapZoom + = std::max({ (float(Settings::map().mGlobalMapCellSize) * 4.f) / float(localWidgetSize), + float(mLocalMap->getWidth()) / (localWidgetSize * (mGrid.width() + 1)), + float(mLocalMap->getHeight()) / (localWidgetSize * (mGrid.height() + 1)) }); if (Settings::map().mGlobal) { @@ -978,7 +986,7 @@ namespace MWGui resizeGlobalMap(); float x = mCurPos.x(), y = mCurPos.y(); - if (mInterior) + if (!mActiveCell->isExterior()) { auto pos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); x = pos.x(); @@ -1021,7 +1029,7 @@ namespace MWGui resizeGlobalMap(); } - MapWindow::~MapWindow() {} + MapWindow::~MapWindow() = default; void MapWindow::setCellName(const std::string& cellName) { @@ -1290,7 +1298,7 @@ namespace MWGui mMarkers.clear(); mGlobalMapRender->clear(); - mChanged = true; + mActiveCell = nullptr; for (auto& widgetPair : mGlobalMapMarkers) MyGUI::Gui::getInstance().destroyWidget(widgetPair.first.widget); diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 5afc8c7c8a..ed070c5407 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -27,6 +27,7 @@ namespace ESM namespace MWWorld { + class Cell; class CellStore; } @@ -77,8 +78,7 @@ namespace MWGui virtual ~LocalMapBase(); void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance = Constants::CellGridRadius); - void setCellPrefix(const std::string& prefix); - void setActiveCell(const int x, const int y, bool interior = false); + void setActiveCell(const MWWorld::Cell& cell); void requestMapRender(const MWWorld::CellStore* cell); void setPlayerDir(const float x, const float y); void setPlayerPos(int cellX, int cellY, const float nx, const float ny); @@ -112,23 +112,17 @@ namespace MWGui protected: void updateLocalMap(); - float mLocalMapZoom = 1.f; MWRender::LocalMap* mLocalMapRender; - - int mCurX, mCurY; // the position of the active cell on the global map (in cell coords) - bool mHasALastActiveCell = false; + const MWWorld::Cell* mActiveCell = nullptr; osg::Vec2f mCurPos; // the position of the player in the world (in cell coords) - bool mInterior; - MyGUI::ScrollView* mLocalMap; - MyGUI::ImageBox* mCompass; - std::string mPrefix; - bool mChanged; - bool mFogOfWarToggled; + MyGUI::ScrollView* mLocalMap = nullptr; + MyGUI::ImageBox* mCompass = nullptr; + float mLocalMapZoom = 1.f; + bool mHasALastActiveCell = false; + bool mFogOfWarToggled = true; bool mFogOfWarEnabled; - - int mNumCells; // for convenience, mCellDistance * 2 + 1 - int mCellDistance; + bool mNeedDoorMarkersUpdate = false; // Stores markers that were placed by a player. May be shared between multiple map views. CustomMarkerCollection& mCustomMarkers; @@ -170,8 +164,7 @@ namespace MWGui MyGUI::IntPoint getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const; MyGUI::IntCoord getMarkerCoordinates( float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const; - MyGUI::Widget* createDoorMarker( - const std::string& name, const MyGUI::VectorString& notes, float x, float y) const; + MyGUI::Widget* createDoorMarker(const std::string& name, float x, float y) const; MyGUI::IntCoord getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const; virtual void notifyPlayerUpdate() {} @@ -189,12 +182,14 @@ namespace MWGui void redraw(); float getWidgetSize() const; - float mMarkerUpdateTimer; + MWGui::LocalMapBase::MapEntry& addMapEntry(); - float mLastDirectionX; - float mLastDirectionY; + MyGUI::IntRect mGrid{ -1, -1, 1, 1 }; + int mExtCellDistance = 0; + float mMarkerUpdateTimer = 0.f; - bool mNeedDoorMarkersUpdate; + float mLastDirectionX = 0.f; + float mLastDirectionY = 0.f; private: void updateDoorMarkers(); diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index c5393fbfb7..a59f225e9e 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -47,13 +47,15 @@ namespace MWGui MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); int categories = MWWorld::ContainerStore::Type_Weapon | MWWorld::ContainerStore::Type_Armor; + std::vector> items; + for (MWWorld::ContainerStoreIterator iter(store.begin(categories)); iter != store.end(); ++iter) { if (iter->getClass().hasItemHealth(*iter)) { int maxDurability = iter->getClass().getItemMaxHealth(*iter); int durability = iter->getClass().getItemHealth(*iter); - if (maxDurability == durability || maxDurability == 0) + if (maxDurability <= durability || maxDurability == 0) continue; int basePrice = iter->getClass().getValue(*iter); @@ -76,22 +78,31 @@ namespace MWGui name += " - " + MyGUI::utility::toString(price) + MWBase::Environment::get().getESMStore()->get().find("sgp")->mValue.getString(); - MyGUI::Button* button = mList->createWidget(price <= playerGold - ? "SandTextButton" - : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip - 0, currentY, 0, lineHeight, MyGUI::Align::Default); - - currentY += lineHeight; - - button->setUserString("Price", MyGUI::utility::toString(price)); - button->setUserData(MWWorld::Ptr(*iter)); - button->setCaptionWithReplacing(name); - button->setSize(mList->getWidth(), lineHeight); - button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); - button->setUserString("ToolTipType", "ItemPtr"); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); + items.emplace_back(name, price, *iter); } } + + std::stable_sort(items.begin(), items.end(), + [](const auto& a, const auto& b) { return Misc::StringUtils::ciLess(std::get<0>(a), std::get<0>(b)); }); + + for (const auto& [name, price, ptr] : items) + { + MyGUI::Button* button = mList->createWidget(price <= playerGold + ? "SandTextButton" + : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip + 0, currentY, 0, lineHeight, MyGUI::Align::Default); + + currentY += lineHeight; + + button->setUserString("Price", MyGUI::utility::toString(price)); + button->setUserData(MWWorld::Ptr(ptr)); + button->setCaptionWithReplacing(name); + button->setSize(mList->getWidth(), lineHeight); + button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); + button->setUserString("ToolTipType", "ItemPtr"); + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); + } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // scrollbar is hidden mList->setVisibleVScroll(false); diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 49d474c826..1d6e1511c4 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -27,7 +28,7 @@ namespace MWGui MessageBoxManager::clear(); } - int MessageBoxManager::getMessagesCount() + std::size_t MessageBoxManager::getMessagesCount() { return mMessageBoxes.size(); } @@ -46,6 +47,20 @@ namespace MWGui mLastButtonPressed = -1; } + void MessageBoxManager::resetInteractiveMessageBox() + { + if (mInterMessageBoxe) + { + mInterMessageBoxe->setVisible(false); + mInterMessageBoxe.reset(); + } + } + + void MessageBoxManager::setLastButtonPressed(int index) + { + mLastButtonPressed = index; + } + void MessageBoxManager::onFrame(float frameDuration) { for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();) @@ -112,7 +127,7 @@ namespace MWGui } bool MessageBoxManager::createInteractiveMessageBox( - std::string_view message, const std::vector& buttons) + std::string_view message, const std::vector& buttons, bool immediate, int defaultFocus) { if (mInterMessageBoxe != nullptr) { @@ -120,7 +135,8 @@ namespace MWGui mInterMessageBoxe->setVisible(false); } - mInterMessageBoxe = std::make_unique(*this, std::string{ message }, buttons); + mInterMessageBoxe + = std::make_unique(*this, std::string{ message }, buttons, immediate, defaultFocus); mLastButtonPressed = -1; return true; @@ -200,13 +216,15 @@ namespace MWGui mMainWidget->setVisible(value); } - InteractiveMessageBox::InteractiveMessageBox( - MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons) + InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, + const std::vector& buttons, bool immediate, int defaultFocus) : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") , mMessageBoxManager(parMessageBoxManager) , mButtonPressed(-1) + , mDefaultFocus(defaultFocus) + , mImmediate(immediate) { int textPadding = 10; // padding between text-widget and main-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget @@ -362,14 +380,17 @@ namespace MWGui MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { - std::vector keywords{ "sOk", "sYes" }; + if (mDefaultFocus >= 0 && mDefaultFocus < static_cast(mButtons.size())) + return mButtons[mDefaultFocus]; + auto& languageManager = MyGUI::LanguageManager::getInstance(); + std::vector keywords{ languageManager.replaceTags("#{sOk}"), + languageManager.replaceTags("#{sYes}") }; + for (MyGUI::Button* button : mButtons) { - for (const std::string& keyword : keywords) + for (const MyGUI::UString& keyword : keywords) { - if (Misc::StringUtils::ciEqual( - MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}").asUTF8(), - button->getCaption().asUTF8())) + if (Misc::StringUtils::ciEqual(keyword, button->getCaption())) { return button; } @@ -393,6 +414,12 @@ namespace MWGui { mButtonPressed = index; mMessageBoxManager.onButtonPressed(mButtonPressed); + if (!mImmediate) + return; + + mMessageBoxManager.setLastButtonPressed(mButtonPressed); + MWBase::Environment::get().getInputManager()->changeInputMode( + MWBase::Environment::get().getWindowManager()->isGuiMode()); return; } index++; diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index b10586549f..feb717e0ad 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -25,10 +25,11 @@ namespace MWGui void onFrame(float frameDuration); void createMessageBox(std::string_view message, bool stat = false); void removeStaticMessageBox(); - bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons); + bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons, + bool immediate = false, int defaultFocus = -1); bool isInteractiveMessageBox(); - int getMessagesCount(); + std::size_t getMessagesCount(); const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe.get(); } @@ -40,6 +41,10 @@ namespace MWGui /// @param reset Reset the pressed button to -1 after reading it. int readPressedButton(bool reset = true); + void resetInteractiveMessageBox(); + + void setLastButtonPressed(int index); + typedef MyGUI::delegates::MultiDelegate EventHandle_Int; // Note: this delegate unassigns itself after it was fired, i.e. works once. @@ -88,7 +93,7 @@ namespace MWGui { public: InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, - const std::vector& buttons); + const std::vector& buttons, bool immediate, int defaultFocus); void mousePressed(MyGUI::Widget* _widget); int readPressedButton(); @@ -107,6 +112,8 @@ namespace MWGui std::vector mButtons; int mButtonPressed; + int mDefaultFocus; + bool mImmediate; }; } diff --git a/apps/openmw/mwgui/mode.hpp b/apps/openmw/mwgui/mode.hpp index 63f81e8b47..44f9743743 100644 --- a/apps/openmw/mwgui/mode.hpp +++ b/apps/openmw/mwgui/mode.hpp @@ -6,7 +6,6 @@ namespace MWGui enum GuiMode { GM_None, - GM_Settings, // Settings window GM_Inventory, // Inventory mode GM_Container, GM_Companion, diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index bfaf011835..fa7bce449b 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -134,7 +134,7 @@ namespace MWGui return false; } else - player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 1); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_PickPocket); return true; } diff --git a/apps/openmw/mwgui/postprocessorhud.cpp b/apps/openmw/mwgui/postprocessorhud.cpp index e54704d6d4..8f5b20ba98 100644 --- a/apps/openmw/mwgui/postprocessorhud.cpp +++ b/apps/openmw/mwgui/postprocessorhud.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -135,9 +136,9 @@ namespace MWGui return; if (enabled) - processor->enableTechnique(technique); + processor->enableTechnique(std::move(technique)); else - processor->disableTechnique(technique); + processor->disableTechnique(std::move(technique)); processor->saveChain(); } } @@ -167,10 +168,11 @@ namespace MWGui if (static_cast(index) != selected) { auto technique = *mActiveList->getItemDataAt>(selected); - if (technique->getDynamic()) + if (technique->getDynamic() || technique->getInternal()) return; - if (processor->enableTechnique(technique, index) != MWRender::PostProcessor::Status_Error) + if (processor->enableTechnique(std::move(technique), index - mOffset) + != MWRender::PostProcessor::Status_Error) processor->saveChain(); } } @@ -421,7 +423,12 @@ namespace MWGui auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + std::vector techniques; for (const auto& [name, _] : processor->getTechniqueMap()) + techniques.push_back(name); + std::sort(techniques.begin(), techniques.end(), Misc::StringUtils::ciLess); + + for (const std::string& name : techniques) { auto technique = processor->loadTechnique(name); @@ -438,10 +445,16 @@ namespace MWGui } } + mOffset = 0; for (auto technique : processor->getTechniques()) { if (!technique->getHidden()) + { mActiveList->addItem(technique->getName(), technique); + + if (technique->getInternal()) + mOffset++; + } } auto tryFocus = [this](ListWrapper* widget, const std::string& hint) { @@ -478,6 +491,7 @@ namespace MWGui factory.registerFactory("Widget"); factory.registerFactory("Widget"); factory.registerFactory("Widget"); + factory.registerFactory("Widget"); factory.registerFactory("Widget"); } } diff --git a/apps/openmw/mwgui/postprocessorhud.hpp b/apps/openmw/mwgui/postprocessorhud.hpp index e08d20e358..20e27bac3a 100644 --- a/apps/openmw/mwgui/postprocessorhud.hpp +++ b/apps/openmw/mwgui/postprocessorhud.hpp @@ -96,6 +96,8 @@ namespace MWGui Gui::AutoSizedEditBox* mShaderInfo; std::string mOverrideHint; + + int mOffset = 0; }; } diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index fb03ab99c9..93b0ef071f 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -85,7 +85,7 @@ namespace MWGui { MWWorld::Ptr item = *mKey[index].button->getUserData(); // Make sure the item is available and is not broken - if (item.isEmpty() || item.getRefData().getCount() < 1 + if (item.isEmpty() || item.getCellRef().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { // Try searching for a compatible replacement @@ -299,7 +299,8 @@ namespace MWGui mSelected->button->setUserString("Spell", spellId.serialize()); // use the icon of the first effect - const ESM::MagicEffect* effect = esmStore.get().find(spell->mEffects.mList.front().mEffectID); + const ESM::MagicEffect* effect + = esmStore.get().find(spell->mEffects.mList.front().mData.mEffectID); std::string path = effect->mIcon; std::replace(path.begin(), path.end(), '/', '\\'); @@ -352,8 +353,7 @@ namespace MWGui bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player) || playerStats.getKnockedDown() || playerStats.getHitRecovery(); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - bool isReturnNeeded = (!godmode && playerStats.isParalyzed()) || playerStats.isDead(); + bool isReturnNeeded = playerStats.isParalyzed() || playerStats.isDead(); if (isReturnNeeded) { @@ -383,12 +383,12 @@ namespace MWGui item = nullptr; // check the item is available and not broken - if (item.isEmpty() || item.getRefData().getCount() < 1 + if (item.isEmpty() || item.getCellRef().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { item = store.findReplacement(key->id); - if (item.isEmpty() || item.getRefData().getCount() < 1) + if (item.isEmpty() || item.getCellRef().getCount() < 1) { MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index da5c0c9ca8..7b445d419f 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -19,7 +20,6 @@ #include "../mwworld/esmstore.hpp" #include "tooltips.hpp" -#include "ustring.hpp" namespace { @@ -114,7 +114,8 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); - okButton->setCaption(toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); updateRaces(); @@ -129,10 +130,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void RaceDialog::onOpen() diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index c2acea93ce..7d57988d97 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -75,7 +75,7 @@ namespace MWGui mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); - bool toolBoxVisible = (gem.getRefData().getCount() != 0); + bool toolBoxVisible = gem.getCellRef().getCount() != 0; mGemBox->setVisible(toolBoxVisible); mGemBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); diff --git a/apps/openmw/mwgui/referenceinterface.cpp b/apps/openmw/mwgui/referenceinterface.cpp index de7c93d862..6dad6b8543 100644 --- a/apps/openmw/mwgui/referenceinterface.cpp +++ b/apps/openmw/mwgui/referenceinterface.cpp @@ -2,14 +2,14 @@ namespace MWGui { - ReferenceInterface::ReferenceInterface() {} + ReferenceInterface::ReferenceInterface() = default; - ReferenceInterface::~ReferenceInterface() {} + ReferenceInterface::~ReferenceInterface() = default; void ReferenceInterface::checkReferenceAvailable() { // check if count of the reference has become 0 - if (!mPtr.isEmpty() && mPtr.getRefData().getCount() == 0) + if (!mPtr.isEmpty() && mPtr.getCellRef().getCount() == 0) { mPtr = MWWorld::Ptr(); onReferenceUnavailable(); diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index aabd3d46ab..c1602b8407 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -49,7 +49,7 @@ namespace MWGui = new SortFilterItemModel(std::make_unique(MWMechanics::getPlayer())); model->setFilter(SortFilterItemModel::Filter_OnlyRepairable); mRepairBox->setModel(model); - + mRepairBox->update(); // Reset scrollbars mRepairBox->resetScrollbars(); } @@ -86,7 +86,7 @@ namespace MWGui mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); - bool toolBoxVisible = (mRepair.getTool().getRefData().getCount() != 0); + bool toolBoxVisible = (mRepair.getTool().getCellRef().getCount() != 0); mToolBox->setVisible(toolBoxVisible); mToolBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); @@ -142,7 +142,7 @@ namespace MWGui void Repair::onRepairItem(MyGUI::Widget* /*sender*/, const MWWorld::Ptr& ptr) { - if (!mRepair.getTool().getRefData().getCount()) + if (!mRepair.getTool().getCellRef().getCount()) return; mRepair.repair(ptr); diff --git a/apps/openmw/mwgui/resourceskin.cpp b/apps/openmw/mwgui/resourceskin.cpp index ea081dd17a..3d9f09952e 100644 --- a/apps/openmw/mwgui/resourceskin.cpp +++ b/apps/openmw/mwgui/resourceskin.cpp @@ -9,15 +9,14 @@ namespace MWGui void resizeSkin(MyGUI::xml::ElementPtr _node) { _node->setAttribute("type", "ResourceSkin"); - const std::string size = _node->findAttribute("size"); - if (!size.empty()) + if (!_node->findAttribute("size").empty()) return; - const std::string textureName = _node->findAttribute("texture"); + auto textureName = _node->findAttribute("texture"); if (textureName.empty()) return; - MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(textureName); + MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(std::string{ textureName }); if (!texture) return; @@ -30,7 +29,7 @@ namespace MWGui if (basis->getName() != "BasisSkin") continue; - const std::string basisSkinType = basis->findAttribute("type"); + auto basisSkinType = basis->findAttribute("type"); if (Misc::StringUtils::ciEqual(basisSkinType, "SimpleText")) continue; bool isTileRect = Misc::StringUtils::ciEqual(basisSkinType, "TileRect"); diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index 04c3806c0e..ddce2c5f50 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -19,7 +20,6 @@ #include "../mwworld/esmstore.hpp" #include "tooltips.hpp" -#include "ustring.hpp" namespace { @@ -148,11 +148,11 @@ namespace MWGui mUpdateSkillArea = true; } - void ReviewDialog::setClass(const ESM::Class& class_) + void ReviewDialog::setClass(const ESM::Class& playerClass) { - mKlass = class_; - mClassWidget->setCaption(mKlass.mName); - ToolTips::createClassToolTip(mClassWidget, mKlass); + mClass = playerClass; + mClassWidget->setCaption(mClass.mName); + ToolTips::createClassToolTip(mClassWidget, mClass); } void ReviewDialog::setBirthSign(const ESM::RefId& signId) @@ -272,7 +272,7 @@ namespace MWGui MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Default); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); - groupWidget->setCaption(toUString(label)); + groupWidget->setCaption(MyGUI::UString(label)); mSkillWidgets.push_back(groupWidget); const int lineHeight = Settings::gui().mFontSize + 2; @@ -287,7 +287,7 @@ namespace MWGui MyGUI::TextBox* skillValueWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); - skillNameWidget->setCaption(toUString(text)); + skillNameWidget->setCaption(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Default); diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index 6f594c60f0..7226ad628d 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -30,7 +30,7 @@ namespace MWGui void setPlayerName(const std::string& name); void setRace(const ESM::RefId& raceId); - void setClass(const ESM::Class& class_); + void setClass(const ESM::Class& playerClass); void setBirthSign(const ESM::RefId& signId); void setHealth(const MWMechanics::DynamicStat& value); @@ -96,7 +96,7 @@ namespace MWGui std::map mSkillWidgetMap; ESM::RefId mRaceId, mBirthSignId; std::string mName; - ESM::Class mKlass; + ESM::Class mClass; std::vector mSkillWidgets; //< Skills and other information bool mUpdateSkillArea; diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index 71b39328f6..94f25e118b 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -7,23 +7,20 @@ #include #include #include +#include #include #include #include - -#include - -#include - -#include - +#include #include #include +#include +#include #include - -#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" @@ -35,7 +32,6 @@ #include "../mwstate/character.hpp" #include "confirmationdialog.hpp" -#include "ustring.hpp" namespace MWGui { @@ -168,7 +164,7 @@ namespace MWGui mCurrentCharacter = mgr->getCurrentCharacter(); - const std::string& directory = Settings::Manager::getString("character", "Saves"); + const std::string& directory = Settings::saves().mCharacter; size_t selectedIndex = MyGUI::ITEM_NONE; @@ -197,8 +193,8 @@ namespace MWGui className = "?"; // From an older savegame format that did not support custom classes properly. } - title << " (#{sLevel} " << signature.mPlayerLevel << " " - << MyGUI::TextIterator::toTagsString(toUString(className)) << ")"; + title << " (#{OMWEngine:Level} " << signature.mPlayerLevel << " " + << MyGUI::TextIterator::toTagsString(MyGUI::UString(className)) << ")"; mCharacterSelection->addItem(MyGUI::LanguageManager::getInstance().replaceTags(title.str())); @@ -302,7 +298,7 @@ namespace MWGui if (mSaving) { - MWBase::Environment::get().getStateManager()->saveGame(mSaveNameEdit->getCaption().asUTF8(), mCurrentSlot); + MWBase::Environment::get().getStateManager()->saveGame(mSaveNameEdit->getCaption(), mCurrentSlot); } else { @@ -367,18 +363,23 @@ namespace MWGui std::string formatTimeplayed(const double timeInSeconds) { - int timePlayed = (int)floor(timeInSeconds); - int days = timePlayed / 60 / 60 / 24; - int hours = (timePlayed / 60 / 60) % 24; - int minutes = (timePlayed / 60) % 60; - int seconds = timePlayed % 60; + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("Interface"); + int duration = static_cast(timeInSeconds); + if (duration <= 0) + return l10n->formatMessage("DurationSecond", { "seconds" }, { 0 }); - std::stringstream stream; - stream << std::setfill('0') << std::setw(2) << days << ":"; - stream << std::setfill('0') << std::setw(2) << hours << ":"; - stream << std::setfill('0') << std::setw(2) << minutes << ":"; - stream << std::setfill('0') << std::setw(2) << seconds; - return stream.str(); + std::string result; + int hours = duration / 3600; + int minutes = (duration / 60) % 60; + int seconds = duration % 60; + if (hours) + result += l10n->formatMessage("DurationHour", { "hours" }, { hours }); + if (minutes) + result += l10n->formatMessage("DurationMinute", { "minutes" }, { minutes }); + if (seconds) + result += l10n->formatMessage("DurationSecond", { "seconds" }, { seconds }); + + return result; } void SaveGameDialog::onSlotSelected(MyGUI::ListBox* sender, size_t pos) @@ -412,7 +413,11 @@ namespace MWGui text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\n"; - text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; + if (mCurrentSlot->mProfile.mMaximumHealth > 0) + text << "#{OMWEngine:Health} " << static_cast(mCurrentSlot->mProfile.mCurrentHealth) << "/" + << static_cast(mCurrentSlot->mProfile.mMaximumHealth) << "\n"; + + text << "#{OMWEngine:Level} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCellName << "}\n"; int hour = int(mCurrentSlot->mProfile.mInGameTime.mGameHour); @@ -422,12 +427,15 @@ namespace MWGui if (hour == 0) hour = 12; + if (mCurrentSlot->mProfile.mCurrentDay > 0) + text << "#{Calendar:day} " << mCurrentSlot->mProfile.mCurrentDay << "\n"; + text << mCurrentSlot->mProfile.mInGameTime.mDay << " " << MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName( mCurrentSlot->mProfile.mInGameTime.mMonth) << " " << hour << " " << (pm ? "#{Calendar:pm}" : "#{Calendar:am}"); - if (Settings::Manager::getBool("timeplayed", "Saves")) + if (mCurrentSlot->mProfile.mTimePlayed > 0) { text << "\n" << "#{OMWEngine:TimePlayed}: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); diff --git a/apps/openmw/mwgui/screenfader.cpp b/apps/openmw/mwgui/screenfader.cpp index e22517a360..0068ba7960 100644 --- a/apps/openmw/mwgui/screenfader.cpp +++ b/apps/openmw/mwgui/screenfader.cpp @@ -96,9 +96,10 @@ namespace MWGui { imageBox->setImageTexture(texturePath); const MyGUI::IntSize imageSize = imageBox->getImageSize(); - imageBox->setImageCoord( - MyGUI::IntCoord(texCoordOverride.left * imageSize.width, texCoordOverride.top * imageSize.height, - texCoordOverride.width * imageSize.width, texCoordOverride.height * imageSize.height)); + imageBox->setImageCoord(MyGUI::IntCoord(static_cast(texCoordOverride.left * imageSize.width), + static_cast(texCoordOverride.top * imageSize.height), + static_cast(texCoordOverride.width * imageSize.width), + static_cast(texCoordOverride.height * imageSize.height))); } } diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index 3debf0d66d..0b1658fd84 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "../mwbase/environment.hpp" @@ -42,17 +43,22 @@ namespace MWGui void ScrollWindow::setPtr(const MWWorld::Ptr& scroll) { - if (scroll.isEmpty() || scroll.getType() != ESM::REC_BOOK) + if (scroll.isEmpty() || (scroll.getType() != ESM::REC_BOOK && scroll.getType() != ESM::REC_BOOK4)) throw std::runtime_error("Invalid argument in ScrollWindow::setPtr"); mScroll = scroll; MWWorld::Ptr player = MWMechanics::getPlayer(); bool showTakeButton = scroll.getContainerStore() != &player.getClass().getContainerStore(player); - MWWorld::LiveCellRef* ref = mScroll.get(); + const std::string* text; + if (scroll.getType() == ESM::REC_BOOK) + text = &scroll.get()->mBase->mText; + else + text = &scroll.get()->mBase->mText; + bool shrinkTextAtLastTag = scroll.getType() == ESM::REC_BOOK; Formatting::BookFormatter formatter; - formatter.markupToWidget(mTextView, ref->mBase->mText, 390, mTextView->getHeight()); + formatter.markupToWidget(mTextView, *text, 390, mTextView->getHeight(), shrinkTextAtLastTag); MyGUI::IntSize size = mTextView->getChildAt(0)->getSize(); // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the diff --git a/apps/openmw/mwgui/settings.cpp b/apps/openmw/mwgui/settings.cpp index fb1068ca41..5005876d55 100644 --- a/apps/openmw/mwgui/settings.cpp +++ b/apps/openmw/mwgui/settings.cpp @@ -99,6 +99,25 @@ namespace MWGui }; } + WindowSettingValues makeDebugWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mDebugX, + .mY = Settings::windows().mDebugY, + .mW = Settings::windows().mDebugW, + .mH = Settings::windows().mDebugH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mDebugMaximizedX, + .mY = Settings::windows().mDebugMaximizedY, + .mW = Settings::windows().mDebugMaximizedW, + .mH = Settings::windows().mDebugMaximizedH, + }, + .mIsMaximized = Settings::windows().mDebugMaximized, + }; + } + WindowSettingValues makeDialogueWindowSettingValues() { return WindowSettingValues{ diff --git a/apps/openmw/mwgui/settings.hpp b/apps/openmw/mwgui/settings.hpp index 8d1cda37dd..b51ac29ce5 100644 --- a/apps/openmw/mwgui/settings.hpp +++ b/apps/openmw/mwgui/settings.hpp @@ -25,6 +25,7 @@ namespace MWGui WindowSettingValues makeCompanionWindowSettingValues(); WindowSettingValues makeConsoleWindowSettingValues(); WindowSettingValues makeContainerWindowSettingValues(); + WindowSettingValues makeDebugWindowSettingValues(); WindowSettingValues makeDialogueWindowSettingValues(); WindowSettingValues makeInventoryWindowSettingValues(); WindowSettingValues makeInventoryBarterWindowSettingValues(); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index b077d9f640..f38f6dc0e1 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -13,6 +12,7 @@ #include #include #include +#include #include #include @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -38,20 +40,25 @@ #include "../mwbase/world.hpp" #include "confirmationdialog.hpp" -#include "ustring.hpp" namespace { - std::string textureMipmappingToStr(const std::string& val) + std::string textureFilteringToStr(const std::string& mipFilter, const std::string& magFilter) { - if (val == "linear") - return "#{OMWEngine:TextureFilteringTrilinear}"; - if (val == "nearest") - return "#{OMWEngine:TextureFilteringBilinear}"; - if (val == "none") + if (mipFilter == "none") return "#{OMWEngine:TextureFilteringDisabled}"; - Log(Debug::Warning) << "Warning: Invalid texture mipmap option: " << val; + if (magFilter == "linear") + { + if (mipFilter == "linear") + return "#{OMWEngine:TextureFilteringTrilinear}"; + if (mipFilter == "nearest") + return "#{OMWEngine:TextureFilteringBilinear}"; + } + else if (magFilter == "nearest") + return "#{OMWEngine:TextureFilteringNearest}"; + + Log(Debug::Warning) << "Warning: Invalid texture filtering options: " << mipFilter << ", " << magFilter; return "#{OMWEngine:TextureFilteringOther}"; } @@ -93,20 +100,6 @@ namespace return left.first > right.first; } - std::string getAspect(int x, int y) - { - int gcd = std::gcd(x, y); - if (gcd == 0) - return std::string(); - - int xaspect = x / gcd; - int yaspect = y / gcd; - // special case: 8 : 5 is usually referred to as 16:10 - if (xaspect == 8 && yaspect == 5) - return "16 : 10"; - return MyGUI::utility::toString(xaspect) + " : " + MyGUI::utility::toString(yaspect); - } - const std::string_view checkButtonType = "CheckButton"; const std::string_view sliderType = "Slider"; @@ -145,9 +138,9 @@ namespace void updateMaxLightsComboBox(MyGUI::ComboBox* box) { constexpr int min = 8; - constexpr int max = 32; + constexpr int max = 64; constexpr int increment = 8; - int maxLights = Settings::Manager::getInt("max lights", "Shaders"); + const int maxLights = Settings::shaders().mMaxLights; // show increments of 8 in dropdown if (maxLights >= min && maxLights <= max && !(maxLights % increment)) box->setIndexSelected((maxLights / increment) - 1); @@ -168,7 +161,7 @@ namespace MWGui std::string_view type = getSettingType(current); if (type == checkButtonType) { - const std::string initialValue + std::string_view initialValue = Settings::get(getSettingCategory(current), getSettingName(current)) ? "#{Interface:On}" : "#{Interface:Off}"; current->castType()->setCaptionWithReplacing(initialValue); @@ -230,14 +223,24 @@ namespace MWGui } } + void SettingsWindow::onFrame(float duration) + { + if (mScriptView->getVisible()) + { + const auto scriptsSize = mScriptAdapter->getSize(); + if (mScriptView->getCanvasSize() != scriptsSize) + mScriptView->setCanvasSize(scriptsSize); + } + } + void SettingsWindow::updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value) { - std::string labelWidgetName = scroller->getUserString("SettingLabelWidget"); + auto labelWidgetName = scroller->getUserString("SettingLabelWidget"); if (!labelWidgetName.empty()) { MyGUI::TextBox* textBox; getWidget(textBox, labelWidgetName); - std::string labelCaption = scroller->getUserString("SettingLabelCaption"); + std::string labelCaption{ scroller->getUserString("SettingLabelCaption") }; labelCaption = Misc::StringUtils::format(labelCaption, value); textBox->setCaptionWithReplacing(labelCaption); } @@ -248,7 +251,7 @@ namespace MWGui , mKeyboardMode(true) , mCurrentPage(-1) { - bool terrain = Settings::Manager::getBool("distant terrain", "Terrain"); + const bool terrain = Settings::terrain().mDistantTerrain; const std::string_view widgetName = terrain ? "RenderingDistanceSlider" : "LargeRenderingDistanceSlider"; MyGUI::Widget* unusedSlider; getWidget(unusedSlider, widgetName); @@ -269,6 +272,9 @@ namespace MWGui getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mKeyboardSwitch, "KeyboardButton"); getWidget(mControllerSwitch, "ControllerButton"); + getWidget(mWaterRefractionButton, "WaterRefractionButton"); + getWidget(mSunlightScatteringButton, "SunlightScatteringButton"); + getWidget(mWobblyShoresButton, "WobblyShoresButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); getWidget(mWaterRainRippleDetail, "WaterRainRippleDetail"); @@ -309,6 +315,8 @@ namespace MWGui += MyGUI::newDelegate(this, &SettingsWindow::onTextureFilteringChanged); mResolutionList->eventListChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onResolutionSelected); + mWaterRefractionButton->eventMouseButtonClick + += MyGUI::newDelegate(this, &SettingsWindow::onRefractionButtonClicked); mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition @@ -344,7 +352,7 @@ namespace MWGui += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); // fill resolution list - int screen = Settings::Manager::getInt("screen", "Video"); + const int screen = Settings::video().mScreen; int numDisplayModes = SDL_GetNumDisplayModes(screen); std::vector> resolutions; for (int i = 0; i < numDisplayModes; i++) @@ -356,21 +364,17 @@ namespace MWGui std::sort(resolutions.begin(), resolutions.end(), sortResolutions); for (std::pair& resolution : resolutions) { - std::string str - = MyGUI::utility::toString(resolution.first) + " x " + MyGUI::utility::toString(resolution.second); - std::string aspect = getAspect(resolution.first, resolution.second); - if (!aspect.empty()) - str = str + " (" + aspect + ")"; + std::string str = Misc::getResolutionText(resolution.first, resolution.second, "%i x %i (%i:%i)"); if (mResolutionList->findItemIndexWith(str) == MyGUI::ITEM_NONE) mResolutionList->addItem(str); } highlightCurrentResolution(); - const std::string& tmip = Settings::general().mTextureMipmap; - mTextureFilteringButton->setCaptionWithReplacing(textureMipmappingToStr(tmip)); + mTextureFilteringButton->setCaptionWithReplacing( + textureFilteringToStr(Settings::general().mTextureMipmap, Settings::general().mTextureMinFilter)); - int waterTextureSize = Settings::Manager::getInt("rtt size", "Water"); + int waterTextureSize = Settings::water().mRttSize; if (waterTextureSize >= 512) mWaterTextureSize->setIndexSelected(0); if (waterTextureSize >= 1024) @@ -378,16 +382,19 @@ namespace MWGui if (waterTextureSize >= 2048) mWaterTextureSize->setIndexSelected(2); - int waterReflectionDetail = std::clamp(Settings::Manager::getInt("reflection detail", "Water"), 0, 5); + const int waterReflectionDetail = Settings::water().mReflectionDetail; mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); - int waterRainRippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2); + const int waterRainRippleDetail = Settings::water().mRainRippleDetail; mWaterRainRippleDetail->setIndexSelected(waterRainRippleDetail); + const bool waterRefraction = Settings::water().mRefraction; + mSunlightScatteringButton->setEnabled(waterRefraction); + mWobblyShoresButton->setEnabled(waterRefraction); + updateMaxLightsComboBox(mMaxLights); - Settings::WindowMode windowMode - = static_cast(Settings::Manager::getInt("window mode", "Video")); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; mWindowBorderButton->setEnabled( windowMode != Settings::WindowMode::Fullscreen && windowMode != Settings::WindowMode::WindowedFullscreen); @@ -401,7 +408,8 @@ namespace MWGui std::vector availableLanguages; const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - for (const auto& path : vfs->getRecursiveDirectoryIterator("l10n/")) + constexpr VFS::Path::NormalizedView l10n("l10n/"); + for (const auto& path : vfs->getRecursiveDirectoryIterator(l10n)) { if (Misc::getFileExtension(path) == "yaml") { @@ -459,7 +467,7 @@ namespace MWGui void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); + setVisible(false); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) @@ -481,8 +489,8 @@ namespace MWGui int resX, resY; parseResolution(resX, resY, resStr); - Settings::Manager::setInt("resolution x", "Video", resX); - Settings::Manager::setInt("resolution y", "Video", resY); + Settings::video().mResolutionX.set(resX); + Settings::video().mResolutionY.set(resY); apply(); } @@ -496,8 +504,8 @@ namespace MWGui { mResolutionList->setIndexSelected(MyGUI::ITEM_NONE); - int currentX = Settings::Manager::getInt("resolution x", "Video"); - int currentY = Settings::Manager::getInt("resolution y", "Video"); + const int currentX = Settings::video().mResolutionX; + const int currentY = Settings::video().mResolutionY; for (size_t i = 0; i < mResolutionList->getItemCount(); ++i) { @@ -512,6 +520,14 @@ namespace MWGui } } + void SettingsWindow::onRefractionButtonClicked(MyGUI::Widget* _sender) + { + const bool refractionEnabled = Settings::water().mRefraction; + + mSunlightScatteringButton->setEnabled(refractionEnabled); + mWobblyShoresButton->setEnabled(refractionEnabled); + } + void SettingsWindow::onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos) { int size = 0; @@ -521,21 +537,19 @@ namespace MWGui size = 1024; else if (pos == 2) size = 2048; - Settings::Manager::setInt("rtt size", "Water", size); + Settings::water().mRttSize.set(size); apply(); } void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { - unsigned int level = static_cast(std::min(pos, 5)); - Settings::Manager::setInt("reflection detail", "Water", level); + Settings::water().mReflectionDetail.set(static_cast(pos)); apply(); } void SettingsWindow::onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { - unsigned int level = static_cast(std::min(pos, 2)); - Settings::Manager::setInt("rain ripple detail", "Water", level); + Settings::water().mRainRippleDetail.set(static_cast(pos)); apply(); } @@ -549,7 +563,8 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->interactiveMessageBox( "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); - Settings::Manager::setString("lighting method", "Shaders", *_sender->getItemDataAt(pos)); + Settings::shaders().mLightingMethod.set( + Settings::parseLightingMethod(*_sender->getItemDataAt(pos))); apply(); } @@ -582,23 +597,22 @@ namespace MWGui "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); } - void SettingsWindow::onVSyncModeChanged(MyGUI::ComboBox* _sender, size_t pos) + void SettingsWindow::onVSyncModeChanged(MyGUI::ComboBox* sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; - int index = static_cast(_sender->getIndexSelected()); - Settings::Manager::setInt("vsync mode", "Video", index); + Settings::video().mVsyncMode.set(static_cast(sender->getIndexSelected())); apply(); } - void SettingsWindow::onWindowModeChanged(MyGUI::ComboBox* _sender, size_t pos) + void SettingsWindow::onWindowModeChanged(MyGUI::ComboBox* sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; - int index = static_cast(_sender->getIndexSelected()); - if (index == static_cast(Settings::WindowMode::WindowedFullscreen)) + const Settings::WindowMode windowMode = static_cast(sender->getIndexSelected()); + if (windowMode == Settings::WindowMode::WindowedFullscreen) { mResolutionList->setEnabled(false); mWindowModeHint->setVisible(true); @@ -609,20 +623,18 @@ namespace MWGui mWindowModeHint->setVisible(false); } - if (index == static_cast(Settings::WindowMode::Windowed)) + if (windowMode == Settings::WindowMode::Windowed) mWindowBorderButton->setEnabled(true); else mWindowBorderButton->setEnabled(false); - Settings::Manager::setInt("window mode", "Video", index); + Settings::video().mWindowMode.set(windowMode); apply(); } void SettingsWindow::onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos) { - int count = 8 * (pos + 1); - - Settings::Manager::setInt("max lights", "Shaders", count); + Settings::shaders().mMaxLights.set(8 * (pos + 1)); apply(); configureWidgets(mMainWidget, false); } @@ -636,6 +648,8 @@ namespace MWGui if (selectedButton == 1 || selectedButton == -1) return; + Settings::shaders().mForcePerPixelLighting.reset(); + Settings::shaders().mClassicFalloff.reset(); Settings::shaders().mLightBoundsMultiplier.reset(); Settings::shaders().mMaximumLightDistance.reset(); Settings::shaders().mLightFadeStart.reset(); @@ -643,8 +657,7 @@ namespace MWGui Settings::shaders().mMaxLights.reset(); Settings::shaders().mLightingMethod.reset(); - const SceneUtil::LightingMethod lightingMethod - = SceneUtil::LightManager::getLightingMethodFromString(Settings::shaders().mLightingMethod); + const SceneUtil::LightingMethod lightingMethod = Settings::shaders().mLightingMethod; const std::size_t lightIndex = mLightingMethodButton->findItemIndexWith(lightingMethodToStr(lightingMethod)); mLightingMethodButton->setIndexSelected(lightIndex); updateMaxLightsComboBox(mMaxLights); @@ -655,18 +668,17 @@ namespace MWGui void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { - MyGUI::UString on = toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On")); + std::string_view on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); bool newState; if (_sender->castType()->getCaption() == on) { - MyGUI::UString off - = toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "Off")); - _sender->castType()->setCaption(off); + _sender->castType()->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "Off"))); newState = false; } else { - _sender->castType()->setCaption(on); + _sender->castType()->setCaption(MyGUI::UString(on)); newState = true; } @@ -680,12 +692,24 @@ namespace MWGui void SettingsWindow::onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos) { - if (pos == 0) - Settings::general().mTextureMipmap.set("nearest"); - else if (pos == 1) - Settings::general().mTextureMipmap.set("linear"); - else - Log(Debug::Warning) << "Unexpected option pos " << pos; + auto& generalSettings = Settings::general(); + switch (pos) + { + case 0: // Bilinear with mips + generalSettings.mTextureMipmap.set("nearest"); + generalSettings.mTextureMagFilter.set("linear"); + generalSettings.mTextureMinFilter.set("linear"); + break; + case 1: // Trilinear with mips + generalSettings.mTextureMipmap.set("linear"); + generalSettings.mTextureMagFilter.set("linear"); + generalSettings.mTextureMinFilter.set("linear"); + break; + default: + Log(Debug::Warning) << "Unexpected texture filtering option pos " << pos; + break; + } + apply(); } @@ -843,14 +867,12 @@ namespace MWGui void SettingsWindow::updateWindowModeSettings() { - size_t index = static_cast(Settings::Manager::getInt("window mode", "Video")); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + const std::size_t windowModeIndex = static_cast(windowMode); - if (index > static_cast(Settings::WindowMode::Windowed)) - index = MyGUI::ITEM_NONE; + mWindowModeList->setIndexSelected(windowModeIndex); - mWindowModeList->setIndexSelected(index); - - if (index != static_cast(Settings::WindowMode::Windowed) && index != MyGUI::ITEM_NONE) + if (windowMode != Settings::WindowMode::Windowed && windowModeIndex != MyGUI::ITEM_NONE) { // check if this resolution is supported in fullscreen if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) @@ -858,8 +880,8 @@ namespace MWGui const std::string& resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); int resX, resY; parseResolution(resX, resY, resStr); - Settings::Manager::setInt("resolution x", "Video", resX); - Settings::Manager::setInt("resolution y", "Video", resY); + Settings::video().mResolutionX.set(resX); + Settings::video().mResolutionY.set(resY); } bool supported = false; @@ -876,8 +898,7 @@ namespace MWGui fallbackY = resY; } - if (resX == Settings::Manager::getInt("resolution x", "Video") - && resY == Settings::Manager::getInt("resolution y", "Video")) + if (resX == Settings::video().mResolutionX && resY == Settings::video().mResolutionY) supported = true; } @@ -885,26 +906,21 @@ namespace MWGui { if (fallbackX != 0 && fallbackY != 0) { - Settings::Manager::setInt("resolution x", "Video", fallbackX); - Settings::Manager::setInt("resolution y", "Video", fallbackY); + Settings::video().mResolutionX.set(fallbackX); + Settings::video().mResolutionY.set(fallbackY); } } mWindowBorderButton->setEnabled(false); } - if (index == static_cast(Settings::WindowMode::WindowedFullscreen)) + if (windowMode == Settings::WindowMode::WindowedFullscreen) mResolutionList->setEnabled(false); } void SettingsWindow::updateVSyncModeSettings() { - int index = static_cast(Settings::Manager::getInt("vsync mode", "Video")); - - if (index < 0 || index > 2) - index = 0; - - mVSyncModeList->setIndexSelected(index); + mVSyncModeList->setIndexSelected(static_cast(Settings::video().mVsyncMode)); } void SettingsWindow::layoutControlsBox() @@ -1005,7 +1021,6 @@ namespace MWGui mScriptDisabled->setVisible(disabled); LuaUi::attachPageAt(mCurrentPage, mScriptAdapter); - mScriptView->setCanvasSize(mScriptAdapter->getSize()); } void SettingsWindow::onScriptFilterChange(MyGUI::EditBox*) @@ -1022,7 +1037,6 @@ namespace MWGui mCurrentPage = *mScriptList->getItemDataAt(index); LuaUi::attachPageAt(mCurrentPage, mScriptAdapter); } - mScriptView->setCanvasSize(mScriptAdapter->getSize()); } void SettingsWindow::onRebindAction(MyGUI::Widget* _sender) diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index ecf9bf0d94..dc4e09f8ac 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -14,6 +14,8 @@ namespace MWGui void onOpen() override; + void onFrame(float duration) override; + void updateControlsBox(); void updateLightSettings(); @@ -35,6 +37,9 @@ namespace MWGui MyGUI::Button* mWindowBorderButton; MyGUI::ComboBox* mTextureFilteringButton; + MyGUI::Button* mWaterRefractionButton; + MyGUI::Button* mSunlightScatteringButton; + MyGUI::Button* mWobblyShoresButton; MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; MyGUI::ComboBox* mWaterRainRippleDetail; @@ -74,6 +79,7 @@ namespace MWGui void onResolutionCancel(); void highlightCurrentResolution(); + void onRefractionButtonClicked(MyGUI::Widget* _sender); void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 4a5e02a881..fe85ea4bd0 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -174,6 +174,7 @@ namespace MWGui : mCategory(Category_All) , mFilter(0) , mSortByType(true) + , mApparatusTypeFilter(-1) { mSourceModel = std::move(sourceModel); } @@ -287,7 +288,7 @@ namespace MWGui if ((mFilter & Filter_OnlyRepairable) && (!base.getClass().hasItemHealth(base) - || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) + || (base.getClass().getItemHealth(base) >= base.getClass().getItemMaxHealth(base)) || (base.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Armor::sRecordId))) return false; @@ -311,6 +312,16 @@ namespace MWGui return false; } + if ((mFilter & Filter_OnlyAlchemyTools)) + { + if (base.getType() != ESM::Apparatus::sRecordId) + return false; + + int32_t apparatusType = base.get()->mBase->mData.mType; + if (mApparatusTypeFilter >= 0 && apparatusType != mApparatusTypeFilter) + return false; + } + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if (compare.find(mNameFilter) == std::string::npos) return false; @@ -352,6 +363,11 @@ namespace MWGui mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } + void SortFilterItemModel::setApparatusTypeFilter(const int32_t type) + { + mApparatusTypeFilter = type; + } + void SortFilterItemModel::update() { mSourceModel->update(); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index 66a22b3afa..d8490f7db1 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -27,6 +27,7 @@ namespace MWGui void setFilter(int filter); void setNameFilter(const std::string& filter); void setEffectFilter(const std::string& filter); + void setApparatusTypeFilter(const int32_t type); /// Use ItemStack::Type for sorting? void setSortByType(bool sort) { mSortByType = sort; } @@ -49,6 +50,7 @@ namespace MWGui static constexpr int Filter_OnlyRepairable = (1 << 5); static constexpr int Filter_OnlyRechargable = (1 << 6); static constexpr int Filter_OnlyRepairTools = (1 << 7); + static constexpr int Filter_OnlyAlchemyTools = (1 << 8); private: std::vector mItems; @@ -59,6 +61,7 @@ namespace MWGui int mFilter; bool mSortByType; + int32_t mApparatusTypeFilter; // filter by apparatus type std::string mNameFilter; // filter by item name std::string mEffectFilter; // filter by magic effect }; diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 1618f34a7a..d8302df87c 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -119,7 +119,7 @@ namespace MWGui void EditEffectDialog::newEffect(const ESM::MagicEffect* effect) { - bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; setMagicEffect(effect); @@ -240,7 +240,7 @@ namespace MWGui // cycle through range types until we find something that's allowed // does not handle the case where nothing is allowed (this should be prevented before opening the Add Effect // dialog) - bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; if (mEffect.mRange == ESM::RT_Self && !allowSelf) @@ -470,9 +470,7 @@ namespace MWGui y *= 1.5; } - ESM::EffectList effectList; - effectList.mList = mEffects; - mSpell.mEffects = effectList; + mSpell.mEffects.populate(mEffects); mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; mSpell.mData.mFlags = 0; @@ -528,10 +526,11 @@ namespace MWGui if (spell->mData.mType != ESM::Spell::ST_Spell) continue; - for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) { + int16_t effectId = effectInfo.mData.mEffectID; const ESM::MagicEffect* effect - = MWBase::Environment::get().getESMStore()->get().find(effectInfo.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effectId); // skip effects that do not allow spellmaking/enchanting int requiredFlags @@ -539,8 +538,8 @@ namespace MWGui if (!(effect->mData.mFlags & requiredFlags)) continue; - if (std::find(knownEffects.begin(), knownEffects.end(), effectInfo.mEffectID) == knownEffects.end()) - knownEffects.push_back(effectInfo.mEffectID); + if (std::find(knownEffects.begin(), knownEffects.end(), effectId) == knownEffects.end()) + knownEffects.push_back(effectId); } } @@ -629,7 +628,7 @@ namespace MWGui const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); - bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; @@ -751,25 +750,9 @@ namespace MWGui void EffectEditorBase::setConstantEffect(bool constant) { mAddEffectDialog.setConstantEffect(constant); + if (!mConstantEffect && constant) + for (ESM::ENAMstruct& effect : mEffects) + effect.mRange = ESM::RT_Self; mConstantEffect = constant; - - if (!constant) - return; - - for (auto it = mEffects.begin(); it != mEffects.end();) - { - if (it->mRange != ESM::RT_Self) - { - auto& store = *MWBase::Environment::get().getESMStore(); - auto magicEffect = store.get().find(it->mEffectID); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) == 0) - { - it = mEffects.erase(it); - continue; - } - it->mRange = ESM::RT_Self; - } - ++it; - } } } diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 5337e2f798..aa29dfc156 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -172,7 +172,7 @@ namespace MWGui w += 16; ToolTipInfo* tooltipInfo = image->getUserData(); - tooltipInfo->text = sourcesDescription; + tooltipInfo->text = std::move(sourcesDescription); // Fade out if (totalDuration >= fadeTime && fadeTime > 0.f) diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index f340d072e0..3d70c391c9 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -48,14 +48,14 @@ namespace MWGui for (const auto& effect : effects.mList) { - short effectId = effect.mEffectID; + short effectId = effect.mData.mEffectID; if (effectId != -1) { const ESM::MagicEffect* magicEffect = store.get().find(effectId); const ESM::Attribute* attribute - = store.get().search(ESM::Attribute::indexToRefId(effect.mAttribute)); - const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(effect.mSkill)); + = store.get().search(ESM::Attribute::indexToRefId(effect.mData.mAttribute)); + const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(effect.mData.mSkill)); std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); @@ -137,7 +137,7 @@ namespace MWGui newSpell.mItem = item; newSpell.mId = item.getCellRef().getRefId(); newSpell.mName = item.getClass().getName(item); - newSpell.mCount = item.getRefData().getCount(); + newSpell.mCount = item.getCellRef().getCount(); newSpell.mType = Spell::Type_EnchantedItem; newSpell.mSelected = invStore.getSelectedEnchantItem() == it; diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index b136a3cad1..d183a00273 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -14,6 +14,7 @@ #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" @@ -91,8 +92,8 @@ namespace MWGui mSpellView->incrementalUpdate(); } - // Update effects in-game too if the window is pinned - if (mPinned && !MWBase::Environment::get().getWindowManager()->isGuiMode()) + // Update effects if the time is unpaused for any reason (e.g. the window is pinned) + if (!MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()) mSpellIcons->updateWidgets(mEffectBox, false); } @@ -236,9 +237,8 @@ namespace MWGui if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; mSpellView->setModel(new SpellModel(MWMechanics::getPlayer())); diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index cb16ab6d15..69f0b4b449 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -32,7 +32,6 @@ #include "../mwmechanics/npcstats.hpp" #include "tooltips.hpp" -#include "ustring.hpp" namespace MWGui { @@ -417,7 +416,7 @@ namespace MWGui MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaption(toUString(label)); + groupWidget->setCaption(MyGUI::UString(label)); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(groupWidget); @@ -433,7 +432,7 @@ namespace MWGui skillNameWidget = mSkillView->createWidget( "SandText", coord1, MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - skillNameWidget->setCaption(toUString(text)); + skillNameWidget->setCaption(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); skillValueWidget = mSkillView->createWidget( @@ -583,9 +582,8 @@ namespace MWGui const std::set& expelled = PCstats.getExpelled(); bool firstFaction = true; - for (auto& factionPair : mFactions) + for (const auto& [factionId, factionRank] : mFactions) { - const ESM::RefId& factionId = factionPair.first; const ESM::Faction* faction = store.get().find(factionId); if (faction->mData.mIsHidden == 1) continue; @@ -612,10 +610,10 @@ namespace MWGui text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - const int rank = std::clamp(factionPair.second, 0, 9); - text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; - - if (rank < 9) + const auto rank = static_cast(std::max(0, factionRank)); + if (rank < faction->mRanks.size()) + text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; + if (rank + 1 < faction->mRanks.size() && !faction->mRanks[rank + 1].empty()) { // player doesn't have max rank yet text += std::string("\n\n#{fontcolourhtml=header}#{sNextRank} ") + faction->mRanks[rank + 1]; diff --git a/apps/openmw/mwgui/textinput.cpp b/apps/openmw/mwgui/textinput.cpp index 18a56e7284..5f47b96f03 100644 --- a/apps/openmw/mwgui/textinput.cpp +++ b/apps/openmw/mwgui/textinput.cpp @@ -5,8 +5,7 @@ #include #include - -#include "ustring.hpp" +#include namespace MWGui { @@ -35,10 +34,10 @@ namespace MWGui if (shown) okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( - toUString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void TextInputDialog::setTextLabel(std::string_view label) diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 6beee8d07b..960a4a5a21 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -119,7 +120,7 @@ namespace MWGui tooltipSize = createToolTip(info, checkOwned()); } else - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); @@ -186,7 +187,7 @@ namespace MWGui if (mFocusObject.isEmpty()) return; - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false, checkOwned()); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") { @@ -210,7 +211,7 @@ namespace MWGui mFocusObject = item; if (!mFocusObject.isEmpty()) - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false); } else if (type == "Spell") { @@ -221,34 +222,36 @@ namespace MWGui = store->get().find(ESM::RefId::deserialize(focus->getUserString("Spell"))); info.caption = spell->mName; Widgets::SpellEffectList effects; - for (const ESM::ENAMstruct& spellEffect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& spellEffect : spell->mEffects.mList) { Widgets::SpellEffectParams params; - params.mEffectID = spellEffect.mEffectID; - params.mSkill = ESM::Skill::indexToRefId(spellEffect.mSkill); - params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute); - params.mDuration = spellEffect.mDuration; - params.mMagnMin = spellEffect.mMagnMin; - params.mMagnMax = spellEffect.mMagnMax; - params.mRange = spellEffect.mRange; - params.mArea = spellEffect.mArea; + params.mEffectID = spellEffect.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); + params.mDuration = spellEffect.mData.mDuration; + params.mMagnMin = spellEffect.mData.mMagnMin; + params.mMagnMax = spellEffect.mData.mMagnMax; + params.mRange = spellEffect.mData.mRange; + params.mArea = spellEffect.mData.mArea; params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability); params.mNoTarget = false; effects.push_back(params); } - if (MWMechanics::spellIncreasesSkill( - spell)) // display school of spells that contribute to skill progress + // display school of spells that contribute to skill progress + if (MWMechanics::spellIncreasesSkill(spell)) { - MWWorld::Ptr player = MWMechanics::getPlayer(); - const auto& school - = store->get().find(MWMechanics::getSpellSchool(spell, player))->mSchool; - info.text = "#{sSchool}: " + MyGUI::TextIterator::toTagsString(school->mName).asUTF8(); + ESM::RefId id = MWMechanics::getSpellSchool(spell, MWMechanics::getPlayer()); + if (!id.empty()) + { + const auto& school = store->get().find(id)->mSchool; + info.text = "#{sSchool}: " + MyGUI::TextIterator::toTagsString(school->mName).asUTF8(); + } } - const std::string& cost = focus->getUserString("SpellCost"); + auto cost = focus->getUserString("SpellCost"); if (!cost.empty() && cost != "0") info.text += MWGui::ToolTips::getValueString(MWMechanics::calcSpellCost(*spell), "#{sCastCost}"); - info.effects = effects; + info.effects = std::move(effects); tooltipSize = createToolTip(info); } else if (type == "Layout") @@ -303,7 +306,7 @@ namespace MWGui { if (!mFocusObject.isEmpty()) { - MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true, checkOwned()); + MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true, checkOwned()); setCoord(viewSize.width / 2 - tooltipSize.width / 2, std::max(0, int(mFocusToolTipY * viewSize.height - tooltipSize.height)), tooltipSize.width, @@ -407,10 +410,13 @@ namespace MWGui const std::string& image = info.icon; int imageSize = (!image.empty()) ? info.imageSize : 0; std::string text = info.text; + std::string_view extra = info.extra; // remove the first newline (easier this way) - if (text.size() > 0 && text[0] == '\n') + if (!text.empty() && text[0] == '\n') text.erase(0, 1); + if (!extra.empty() && extra[0] == '\n') + extra = extra.substr(1); const ESM::Enchantment* enchant = nullptr; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -478,10 +484,13 @@ namespace MWGui MyGUI::IntCoord(padding.left + 8 + 4, totalSize.height + padding.top, 300 - padding.left - 8 - 4, 300 - totalSize.height), MyGUI::Align::Default); - edit->setEditMultiLine(true); - edit->setEditWordWrap(true); - edit->setCaption(note); - edit->setSize(edit->getWidth(), edit->getTextSize().height); + constexpr size_t maxLength = 60; + std::string shortenedNote = note.substr(0, std::min(maxLength, note.find('\n'))); + if (shortenedNote.size() < note.size()) + shortenedNote += " ..."; + edit->setCaption(shortenedNote); + MyGUI::IntSize noteTextSize = edit->getTextSize(); + edit->setSize(std::max(edit->getWidth(), noteTextSize.width), noteTextSize.height); icon->setPosition(icon->getLeft(), (edit->getTop() + edit->getBottom()) / 2 - icon->getHeight() / 2); totalSize.height += std::max(edit->getHeight(), icon->getHeight()); totalSize.width = std::max(totalSize.width, edit->getWidth() + 8 + 4); @@ -566,6 +575,24 @@ namespace MWGui } } + if (!extra.empty()) + { + Gui::EditBox* extraWidget = mDynamicToolTipBox->createWidget("SandText", + MyGUI::IntCoord(padding.left, totalSize.height + 12, 300 - padding.left, 300 - totalSize.height), + MyGUI::Align::Stretch, "ToolTipExtraText"); + + extraWidget->setEditStatic(true); + extraWidget->setEditMultiLine(true); + extraWidget->setEditWordWrap(info.wordWrap); + extraWidget->setCaptionWithReplacing(extra); + extraWidget->setTextAlign(MyGUI::Align::HCenter | MyGUI::Align::Top); + extraWidget->setNeedKeyFocus(false); + + MyGUI::IntSize extraTextSize = extraWidget->getTextSize(); + totalSize.height += extraTextSize.height + 4; + totalSize.width = std::max(totalSize.width, extraTextSize.width); + } + captionWidget->setCoord((totalSize.width - captionSize.width) / 2 + imageSize, (captionHeight - captionSize.height) / 2, captionSize.width - imageSize, captionSize.height); @@ -953,8 +980,7 @@ namespace MWGui widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + MyGUI::TextIterator::toTagsString( - store->get().find(effect->mData.mSchool)->mSchool->mName) - .asUTF8()); + store->get().find(effect->mData.mSchool)->mSchool->mName)); widget->setUserString("ImageTexture_MagicEffectImage", icon); } } diff --git a/apps/openmw/mwgui/tooltips.hpp b/apps/openmw/mwgui/tooltips.hpp index 69f6856840..132698475f 100644 --- a/apps/openmw/mwgui/tooltips.hpp +++ b/apps/openmw/mwgui/tooltips.hpp @@ -29,6 +29,7 @@ namespace MWGui std::string caption; std::string text; + std::string extra; std::string icon; int imageSize; diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index c62d360412..ba752303d2 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -42,6 +43,75 @@ namespace return static_cast(price * count); } + bool haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer) + { + // accept if merchant offer is better than player offer + if (playerOffer <= merchantOffer) + { + return true; + } + + // reject if npc is a creature + if (merchant.getType() != ESM::NPC::sRecordId) + { + return false; + } + + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + + // Is the player buying? + bool buying = (merchantOffer < 0); + int a = std::abs(merchantOffer); + int b = std::abs(playerOffer); + int d = (buying) ? int(100 * (a - b) / a) : int(100 * (b - a) / b); + + int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(merchant); + + const MWMechanics::CreatureStats& merchantStats = merchant.getClass().getCreatureStats(merchant); + const MWMechanics::CreatureStats& playerStats = player.getClass().getCreatureStats(player); + + float a1 = static_cast(player.getClass().getSkill(player, ESM::Skill::Mercantile)); + float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); + float d1 = static_cast(merchant.getClass().getSkill(merchant, ESM::Skill::Mercantile)); + float e1 = 0.1f * merchantStats.getAttribute(ESM::Attribute::Luck).getModified(); + float f1 = 0.2f * merchantStats.getAttribute(ESM::Attribute::Personality).getModified(); + + float dispositionTerm = gmst.find("fDispositionMod")->mValue.getFloat() * (clampedDisposition - 50); + float pcTerm = (dispositionTerm + a1 + b1 + c1) * playerStats.getFatigueTerm(); + float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); + float x = gmst.find("fBargainOfferMulti")->mValue.getFloat() * d + + gmst.find("fBargainOfferBase")->mValue.getFloat() + int(pcTerm - npcTerm); + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::rollDice(100, prng) + 1; + + // reject if roll fails + // (or if player tries to buy things and get money) + if (roll > x || (merchantOffer < 0 && 0 < playerOffer)) + { + return false; + } + + // apply skill gain on successful barter + float skillGain = 0.f; + int finalPrice = std::abs(playerOffer); + int initialMerchantOffer = std::abs(merchantOffer); + + if (!buying && (finalPrice > initialMerchantOffer)) + { + skillGain = std::floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); + } + else if (buying && (finalPrice < initialMerchantOffer)) + { + skillGain = std::floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); + } + player.getClass().skillUsageSucceeded( + player, ESM::Skill::Mercantile, ESM::Skill::Mercantile_Success, skillGain); + + return true; + } } namespace MWGui @@ -328,7 +398,7 @@ namespace MWGui } } - bool offerAccepted = mTrading.haggle(player, mPtr, mCurrentBalance, mCurrentMerchantOffer); + bool offerAccepted = haggle(player, mPtr, mCurrentBalance, mCurrentMerchantOffer); // apply disposition change if merchant is NPC if (mPtr.getClass().isNpc()) diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index 7d5fd399df..33c39cb269 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -1,8 +1,6 @@ #ifndef MWGUI_TRADEWINDOW_H #define MWGUI_TRADEWINDOW_H -#include "../mwmechanics/trading.hpp" - #include "referenceinterface.hpp" #include "windowbase.hpp" @@ -53,7 +51,6 @@ namespace MWGui ItemView* mItemView; SortFilterItemModel* mSortModel; TradeItemModel* mTradeModel; - MWMechanics::Trading mTrading; static const float sBalanceChangeInitialPause; // in seconds static const float sBalanceChangeInterval; // in seconds diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index c915619a1a..890aa0ba68 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -5,6 +5,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -167,17 +168,14 @@ namespace MWGui // You can not train a skill above its governing attribute if (pcStats.getSkill(skill->mId).getBase() - >= pcStats.getAttribute(ESM::Attribute::indexToRefId(skill->mData.mAttribute)).getBase()) + >= pcStats.getAttribute(ESM::Attribute::indexToRefId(skill->mData.mAttribute)).getModified()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage17}"); return; } // increase skill - MWWorld::LiveCellRef* playerRef = player.get(); - - const ESM::Class* class_ = store.get().find(playerRef->mBase->mClass); - pcStats.increaseSkill(skill->mId, *class_, true); + MWBase::Environment::get().getLuaManager()->skillLevelUp(player, skill->mId, "trainer"); // remove gold player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price); @@ -191,8 +189,8 @@ namespace MWGui mProgressBar.setProgress(0, 2); mTimeAdvancer.run(2); - MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2); - MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2, false, 0.2); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2f); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f, false, 0.2f); } void TrainingWindow::onTrainingProgressChanged(int cur, int total) diff --git a/apps/openmw/mwgui/ustring.hpp b/apps/openmw/mwgui/ustring.hpp deleted file mode 100644 index 5a6c30312a..0000000000 --- a/apps/openmw/mwgui/ustring.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef MWGUI_USTRING_H -#define MWGUI_USTRING_H - -#include - -namespace MWGui -{ - // FIXME: Remove once we get a version of MyGUI that supports string_view - inline MyGUI::UString toUString(std::string_view string) - { - return { string.data(), string.size() }; - } -} - -#endif \ No newline at end of file diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index ab17031168..568f05abc3 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include "../mwbase/environment.hpp" @@ -186,7 +186,7 @@ namespace MWGui void WaitDialog::startWaiting(int hoursToWait) { - if (Settings::Manager::getBool("autosave", "Saves")) // autosaves when enabled + if (Settings::saves().mAutosave) // autosaves when enabled MWBase::Environment::get().getStateManager()->quickSave("Autosave"); MWBase::World* world = MWBase::Environment::get().getWorld(); diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index debacfbed9..6cc5bdfdf5 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -20,8 +21,6 @@ #include "../mwworld/esmstore.hpp" -#include "ustring.hpp" - namespace MWGui::Widgets { /* MWSkill */ @@ -135,8 +134,7 @@ namespace MWGui::Widgets } else { - MyGUI::UString name = toUString(attribute->mName); - mAttributeNameWidget->setCaption(name); + mAttributeNameWidget->setCaption(MyGUI::UString(attribute->mName)); } } if (mAttributeValueWidget) @@ -197,18 +195,18 @@ namespace MWGui::Widgets const ESM::Spell* spell = store.get().search(mId); MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); - for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) { MWSpellEffectPtr effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); SpellEffectParams params; - params.mEffectID = effectInfo.mEffectID; - params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); - params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); - params.mDuration = effectInfo.mDuration; - params.mMagnMin = effectInfo.mMagnMin; - params.mMagnMax = effectInfo.mMagnMax; - params.mRange = effectInfo.mRange; + params.mEffectID = effectInfo.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute); + params.mDuration = effectInfo.mData.mDuration; + params.mMagnMin = effectInfo.mData.mMagnMin; + params.mMagnMax = effectInfo.mData.mMagnMax; + params.mRange = effectInfo.mData.mRange; params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0; params.mNoTarget = (flags & MWEffectList::EF_NoTarget); params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude); @@ -310,17 +308,17 @@ namespace MWGui::Widgets SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects) { SpellEffectList result; - for (const ESM::ENAMstruct& effectInfo : effects->mList) + for (const ESM::IndexedENAMstruct& effectInfo : effects->mList) { SpellEffectParams params; - params.mEffectID = effectInfo.mEffectID; - params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); - params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); - params.mDuration = effectInfo.mDuration; - params.mMagnMin = effectInfo.mMagnMin; - params.mMagnMax = effectInfo.mMagnMax; - params.mRange = effectInfo.mRange; - params.mArea = effectInfo.mArea; + params.mEffectID = effectInfo.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute); + params.mDuration = effectInfo.mData.mDuration; + params.mMagnMin = effectInfo.mData.mMagnMin; + params.mMagnMax = effectInfo.mData.mMagnMax; + params.mRange = effectInfo.mData.mRange; + params.mArea = effectInfo.mData.mArea; result.push_back(params); } return result; @@ -355,12 +353,10 @@ namespace MWGui::Widgets const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::MagicEffect* magicEffect = store.get().search(mEffectParams.mEffectID); + const ESM::MagicEffect* magicEffect = store.get().find(mEffectParams.mEffectID); const ESM::Attribute* attribute = store.get().search(mEffectParams.mAttribute); const ESM::Skill* skill = store.get().search(mEffectParams.mSkill); - assert(magicEffect); - auto windowManager = MWBase::Environment::get().getWindowManager(); std::string_view pt = windowManager->getGameSettingString("spoint", {}); @@ -375,7 +371,7 @@ namespace MWGui::Widgets std::string spellLine = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); - if (mEffectParams.mMagnMin || mEffectParams.mMagnMax) + if ((mEffectParams.mMagnMin || mEffectParams.mMagnMax) && !mEffectParams.mNoMagnitude) { ESM::MagicEffect::MagnitudeDisplayType displayType = magicEffect->getMagnitudeDisplayType(); if (displayType == ESM::MagicEffect::MDT_TimesInt) @@ -390,7 +386,7 @@ namespace MWGui::Widgets spellLine += formatter.str(); } - else if (displayType != ESM::MagicEffect::MDT_None && !mEffectParams.mNoMagnitude) + else if (displayType != ESM::MagicEffect::MDT_None) { spellLine += " " + MyGUI::utility::toString(mEffectParams.mMagnMin); if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) @@ -499,13 +495,13 @@ namespace MWGui::Widgets { std::stringstream out; out << mValue << "/" << mMax; - mBarTextWidget->setCaption(out.str().c_str()); + mBarTextWidget->setCaption(out.str()); } } void MWDynamicStat::setTitle(std::string_view text) { if (mTextWidget) - mTextWidget->setCaption(toUString(text)); + mTextWidget->setCaption(MyGUI::UString(text)); } MWDynamicStat::~MWDynamicStat() {} diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 4c191eaeb8..f5d90590f8 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -58,7 +58,7 @@ void WindowBase::setVisible(bool visible) onClose(); } -bool WindowBase::isVisible() +bool WindowBase::isVisible() const { return mMainWidget->getVisible(); } @@ -77,6 +77,34 @@ void WindowBase::center() mMainWidget->setCoord(coord); } +void WindowBase::clampWindowCoordinates(MyGUI::Window* window) +{ + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + if (window->getLayer()) + viewSize = window->getLayer()->getSize(); + + // Window's minimum size is larger than the screen size, can not clamp coordinates + auto minSize = window->getMinSize(); + if (minSize.width > viewSize.width || minSize.height > viewSize.height) + return; + + int left = std::max(0, window->getPosition().left); + int top = std::max(0, window->getPosition().top); + int width = std::clamp(window->getSize().width, 0, viewSize.width); + int height = std::clamp(window->getSize().height, 0, viewSize.height); + if (left + width > viewSize.width) + left = viewSize.width - width; + + if (top + height > viewSize.height) + top = viewSize.height - height; + + if (window->getSize().width != width || window->getSize().height != height) + window->setSize(width, height); + + if (window->getPosition().left != left || window->getPosition().top != top) + window->setPosition(left, top); +} + WindowModal::WindowModal(const std::string& parLayout) : WindowBase(parLayout) { diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 88b46b0bd2..466060c6ad 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -37,7 +37,7 @@ namespace MWGui /// Sets the visibility of the window void setVisible(bool visible) override; /// Returns the visibility state of the window - bool isVisible(); + bool isVisible() const; void center(); @@ -52,6 +52,8 @@ namespace MWGui virtual std::string_view getWindowIdForLua() const { return ""; } void setDisabledByLua(bool disabled) { mDisabledByLua = disabled; } + static void clampWindowCoordinates(MyGUI::Window* window); + protected: virtual void onTitleDoubleClicked(); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index b9c52136c1..1816cf8a61 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -51,6 +51,7 @@ #include #include +#include #include @@ -117,7 +118,6 @@ #include "tradewindow.hpp" #include "trainingwindow.hpp" #include "travelwindow.hpp" -#include "ustring.hpp" #include "videowidget.hpp" #include "waitdialog.hpp" @@ -294,8 +294,7 @@ namespace MWGui += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); mVideoWrapper = std::make_unique(window, viewer); - mVideoWrapper->setGammaContrast( - Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); + mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); if (useShaders) mGuiPlatform->getRenderManagerPtr()->enableShaders(mResourceSystem->getSceneManager()->getShaderManager()); @@ -357,7 +356,8 @@ namespace MWGui mWindows.push_back(std::move(console)); trackWindow(mConsole, makeConsoleWindowSettingValues()); - bool questList = mResourceSystem->getVFS()->exists("textures/tx_menubook_options_over.dds"); + constexpr VFS::Path::NormalizedView menubookOptionsOverTexture("textures/tx_menubook_options_over.dds"); + const bool questList = mResourceSystem->getVFS()->exists(menubookOptionsOverTexture); auto journal = JournalWindow::create(JournalViewModel::create(), questList, mEncoding); mGuiModeStates[GM_Journal] = GuiModeState(journal.get()); mWindows.push_back(std::move(journal)); @@ -410,7 +410,6 @@ namespace MWGui mSettingsWindow = settingsWindow.get(); mWindows.push_back(std::move(settingsWindow)); trackWindow(mSettingsWindow, makeSettingsWindowSettingValues()); - mGuiModeStates[GM_Settings] = GuiModeState(mSettingsWindow); auto confirmationDialog = std::make_unique(); mConfirmationDialog = confirmationDialog.get(); @@ -498,6 +497,7 @@ namespace MWGui auto debugWindow = std::make_unique(); mDebugWindow = debugWindow.get(); mWindows.push_back(std::move(debugWindow)); + trackWindow(mDebugWindow, makeDebugWindowSettingValues()); auto postProcessorHud = std::make_unique(); mPostProcessorHud = postProcessorHud.get(); @@ -548,7 +548,8 @@ namespace MWGui { try { - LuaUi::clearUserInterface(); + LuaUi::clearGameInterface(); + LuaUi::clearMenuInterface(); mStatsWatcher.reset(); @@ -744,9 +745,9 @@ namespace MWGui } void WindowManager::interactiveMessageBox( - std::string_view message, const std::vector& buttons, bool block) + std::string_view message, const std::vector& buttons, bool block, int defaultFocus) { - mMessageBoxManager->createInteractiveMessageBox(message, buttons); + mMessageBoxManager->createInteractiveMessageBox(message, buttons, block, defaultFocus); updateVisible(); if (block) @@ -779,6 +780,8 @@ namespace MWGui frameRateLimiter.limit(); } + + mMessageBoxManager->resetInteractiveMessageBox(); } } @@ -786,8 +789,8 @@ namespace MWGui { if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) { - MyGUI::UString text = MyGUI::LanguageManager::getInstance().replaceTags(toUString(message)); - mDialogueWindow->addMessageBox(text.asUTF8()); + MyGUI::UString text = MyGUI::LanguageManager::getInstance().replaceTags(MyGUI::UString(message)); + mDialogueWindow->addMessageBox(text); } else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) { @@ -842,7 +845,7 @@ namespace MWGui if (!player.getCell()->isExterior()) { - setActiveMap(x, y, true); + setActiveMap(*player.getCell()->getCell()); } // else: need to know the current grid center, call setActiveMap from changeCell @@ -912,6 +915,9 @@ namespace MWGui if (isConsoleMode()) mConsole->onFrame(frameDuration); + if (isSettingsWindowVisible()) + mSettingsWindow->onFrame(frameDuration); + if (!gameRunning) return; @@ -980,29 +986,23 @@ namespace MWGui mMap->addVisitedLocation(name, cellCommon->getGridX(), cellCommon->getGridY()); mMap->cellExplored(cellCommon->getGridX(), cellCommon->getGridY()); - - setActiveMap(cellCommon->getGridX(), cellCommon->getGridY(), false); } else { - mMap->setCellPrefix(std::string(cellCommon->getNameId())); - mHud->setCellPrefix(std::string(cellCommon->getNameId())); - osg::Vec3f worldPos; if (!MWBase::Environment::get().getWorld()->findInteriorPositionInWorldSpace(cell, worldPos)) worldPos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); else MWBase::Environment::get().getWorld()->getPlayer().setLastKnownExteriorPosition(worldPos); mMap->setGlobalMapPlayerPosition(worldPos.x(), worldPos.y()); - - setActiveMap(0, 0, true); } + setActiveMap(*cellCommon); } - void WindowManager::setActiveMap(int x, int y, bool interior) + void WindowManager::setActiveMap(const MWWorld::Cell& cell) { - mMap->setActiveCell(x, y, interior); - mHud->setActiveCell(x, y, interior); + mMap->setActiveCell(cell); + mHud->setActiveCell(cell); } void WindowManager::setDrowningBarVisibility(bool visible) @@ -1087,7 +1087,7 @@ namespace MWGui void WindowManager::onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result) { - std::string_view tag = _tag.asUTF8(); + std::string_view tag = _tag; std::string_view MyGuiPrefix = "setting="; @@ -1097,10 +1097,13 @@ namespace MWGui { tag = tag.substr(MyGuiPrefix.length()); size_t comma_pos = tag.find(','); + if (comma_pos == std::string_view::npos) + throw std::runtime_error("Invalid setting tag (expected comma): " + std::string(tag)); + std::string_view settingSection = tag.substr(0, comma_pos); std::string_view settingTag = tag.substr(comma_pos + 1, tag.length()); - _result = Settings::Manager::getString(settingTag, settingSection); + _result = Settings::get(settingSection, settingTag).get().print(); } else if (tag.starts_with(tokenToFind)) { @@ -1115,7 +1118,7 @@ namespace MWGui else { std::vector split; - Misc::StringUtils::split(std::string{ tag }, split, ":"); + Misc::StringUtils::split(tag, split, ":"); l10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager(); @@ -1156,25 +1159,22 @@ namespace MWGui changeRes = true; else if (setting.first == "Video" && setting.second == "vsync mode") - mVideoWrapper->setSyncToVBlank(Settings::Manager::getInt("vsync mode", "Video")); + mVideoWrapper->setSyncToVBlank(Settings::video().mVsyncMode); else if (setting.first == "Video" && (setting.second == "gamma" || setting.second == "contrast")) - mVideoWrapper->setGammaContrast( - Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); + mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); } if (changeRes) { - mVideoWrapper->setVideoMode(Settings::Manager::getInt("resolution x", "Video"), - Settings::Manager::getInt("resolution y", "Video"), - static_cast(Settings::Manager::getInt("window mode", "Video")), - Settings::Manager::getBool("window border", "Video")); + mVideoWrapper->setVideoMode(Settings::video().mResolutionX, Settings::video().mResolutionY, + Settings::video().mWindowMode, Settings::video().mWindowBorder); } } void WindowManager::windowResized(int x, int y) { - Settings::Manager::setInt("resolution x", "Video", x); - Settings::Manager::setInt("resolution y", "Video", y); + Settings::video().mResolutionX.set(x); + Settings::video().mResolutionY.set(y); // We only want to process changes to window-size related settings. Settings::CategorySettingVector filter = { { "Video", "resolution x" }, { "Video", "resolution y" } }; @@ -1204,6 +1204,8 @@ namespace MWGui const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; window->setPosition(MyGUI::IntPoint(static_cast(rect.mX * x), static_cast(rect.mY * y))); window->setSize(MyGUI::IntSize(static_cast(rect.mW * x), static_cast(rect.mH * y))); + + WindowBase::clampWindowCoordinates(window); } for (const auto& window : mWindows) @@ -1227,7 +1229,7 @@ namespace MWGui MWBase::Environment::get().getStateManager()->requestQuit(); } - void WindowManager::onCursorChange(const std::string& name) + void WindowManager::onCursorChange(std::string_view name) { mCursorManager->cursorChanged(name); } @@ -1304,7 +1306,7 @@ namespace MWGui return mViewer->getCamera()->getCullMask(); } - void WindowManager::popGuiMode() + void WindowManager::popGuiMode(bool forceExit) { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) { @@ -1314,10 +1316,19 @@ namespace MWGui if (!mGuiModes.empty()) { const GuiMode mode = mGuiModes.back(); + if (forceExit) + { + GuiModeState& state = mGuiModeStates[mode]; + for (const auto& window : state.mWindows) + window->exit(); + } mKeyboardNavigation->saveFocus(mode); - mGuiModes.pop_back(); - mGuiModeStates[mode].update(false); - MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); + if (containsMode(mode)) + { + mGuiModes.pop_back(); + mGuiModeStates[mode].update(false); + MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); + } } if (!mGuiModes.empty()) @@ -1539,8 +1550,7 @@ namespace MWGui bool WindowManager::isGuiMode() const { - return !mGuiModes.empty() || isConsoleMode() || (mPostProcessorHud && mPostProcessorHud->isVisible()) - || (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); + return !mGuiModes.empty() || isConsoleMode() || isPostProcessorHudVisible() || isInteractiveMessageBoxActive(); } bool WindowManager::isConsoleMode() const @@ -1550,7 +1560,17 @@ namespace MWGui bool WindowManager::isPostProcessorHudVisible() const { - return mPostProcessorHud->isVisible(); + return mPostProcessorHud && mPostProcessorHud->isVisible(); + } + + bool WindowManager::isSettingsWindowVisible() const + { + return mSettingsWindow && mSettingsWindow->isVisible(); + } + + bool WindowManager::isInteractiveMessageBoxActive() const + { + return mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox(); } MWGui::GuiMode WindowManager::getMode() const @@ -1600,9 +1620,9 @@ namespace MWGui mQuickKeysMenu->activateQuickKey(index); } - bool WindowManager::toggleHud() + bool WindowManager::setHudVisibility(bool show) { - mHudEnabled = !mHudEnabled; + mHudEnabled = show; updateVisible(); mMessageBoxManager->setVisible(mHudEnabled); return mHudEnabled; @@ -1670,7 +1690,10 @@ namespace MWGui void WindowManager::onKeyFocusChanged(MyGUI::Widget* widget) { - if (widget && widget->castType(false)) + bool isEditBox = widget && widget->castType(false); + LuaUi::WidgetExtension* luaWidget = dynamic_cast(widget); + bool capturesInput = luaWidget ? luaWidget->isTextInput() : isEditBox; + if (widget && capturesInput) SDL_StartTextInput(); else SDL_StopTextInput(); @@ -1681,9 +1704,9 @@ namespace MWGui mHud->setEnemy(enemy); } - int WindowManager::getMessagesCount() const + std::size_t WindowManager::getMessagesCount() const { - int count = 0; + std::size_t count = 0; if (mMessageBoxManager) count = mMessageBoxManager->getMessagesCount(); @@ -1706,13 +1729,15 @@ namespace MWGui const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; - layout->mMainWidget->setPosition( + MyGUI::Window* window = layout->mMainWidget->castType(); + window->setPosition( MyGUI::IntPoint(static_cast(rect.mX * viewSize.width), static_cast(rect.mY * viewSize.height))); - layout->mMainWidget->setSize( + window->setSize( MyGUI::IntSize(static_cast(rect.mW * viewSize.width), static_cast(rect.mH * viewSize.height))); - MyGUI::Window* window = layout->mMainWidget->castType(); window->eventWindowChangeCoord += MyGUI::newDelegate(this, &WindowManager::onWindowChangeCoord); + WindowBase::clampWindowCoordinates(window); + mTrackedWindows.emplace(window, settings); } @@ -1742,6 +1767,8 @@ namespace MWGui if (it == mTrackedWindows.end()) return; + WindowBase::clampWindowCoordinates(window); + const WindowSettingValues& settings = it->second; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); @@ -2067,13 +2094,13 @@ namespace MWGui mWerewolfFader->notifyAlphaChanged(set ? 1.0f : 0.0f); } - void WindowManager::onClipboardChanged(const std::string& _type, const std::string& _data) + void WindowManager::onClipboardChanged(std::string_view _type, std::string_view _data) { if (_type == "Text") SDL_SetClipboardText(MyGUI::TextIterator::getOnlyText(MyGUI::UString(_data)).asUTF8().c_str()); } - void WindowManager::onClipboardRequested(const std::string& _type, std::string& _data) + void WindowManager::onClipboardRequested(std::string_view _type, std::string& _data) { if (_type != "Text") return; @@ -2126,6 +2153,21 @@ namespace MWGui updateVisible(); } + void WindowManager::toggleSettingsWindow() + { + bool visible = mSettingsWindow->isVisible(); + + if (!visible && !mGuiModes.empty()) + mKeyboardNavigation->saveFocus(mGuiModes.back()); + + mSettingsWindow->setVisible(!visible); + + if (visible && !mGuiModes.empty()) + mKeyboardNavigation->restoreFocus(mGuiModes.back()); + + updateVisible(); + } + void WindowManager::cycleSpell(bool next) { if (!isGuiMode()) @@ -2168,11 +2210,16 @@ namespace MWGui mConsole->print(msg, color); } - void WindowManager::setConsoleMode(const std::string& mode) + void WindowManager::setConsoleMode(std::string_view mode) { mConsole->setConsoleMode(mode); } + const std::string& WindowManager::getConsoleMode() + { + return mConsole->getConsoleMode(); + } + void WindowManager::createCursors() { MyGUI::ResourceManager::EnumeratorPtr enumerator = MyGUI::ResourceManager::getInstance().getEnumerator(); @@ -2182,9 +2229,10 @@ namespace MWGui ResourceImageSetPointerFix* imgSetPointer = resource->castType(false); if (!imgSetPointer) continue; - std::string tex_name = imgSetPointer->getImageSet()->getIndexInfo(0, 0).texture; - osg::ref_ptr image = mResourceSystem->getImageManager()->getImage(tex_name); + const VFS::Path::Normalized path(imgSetPointer->getImageSet()->getIndexInfo(0, 0).texture); + + osg::ref_ptr image = mResourceSystem->getImageManager()->getImage(path); if (image.valid()) { diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 9d79e0a0d7..3ca863127b 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -50,6 +50,7 @@ namespace MyGUI namespace MWWorld { + class Cell; class ESMStore; } @@ -149,7 +150,7 @@ namespace MWGui void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) override; void pushGuiMode(GuiMode mode) override; - void popGuiMode() override; + void popGuiMode(bool forceExit = false) override; void removeGuiMode(GuiMode mode) override; ///< can be anywhere in the stack void goToJail(int days) override; @@ -160,8 +161,9 @@ namespace MWGui bool isGuiMode() const override; bool isConsoleMode() const override; - bool isPostProcessorHudVisible() const override; + bool isSettingsWindowVisible() const override; + bool isInteractiveMessageBoxActive() const override; void toggleVisible(GuiWindow wnd) override; @@ -191,7 +193,8 @@ namespace MWGui void setConsoleSelectedObject(const MWWorld::Ptr& object) override; MWWorld::Ptr getConsoleSelectedObject() const override; void printToConsole(const std::string& msg, std::string_view color) override; - void setConsoleMode(const std::string& mode) override; + void setConsoleMode(std::string_view mode) override; + const std::string& getConsoleMode() override; /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning @@ -214,9 +217,6 @@ namespace MWGui bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) bool getFullHelp() const override; - void setActiveMap(int x, int y, bool interior) override; - ///< set the indices of the map texture that should be used - /// sets the visibility of the drowning bar void setDrowningBarVisibility(bool visible) override; @@ -247,7 +247,8 @@ namespace MWGui void showCrosshair(bool show) override; /// Turn visibility of HUD on or off - bool toggleHud() override; + bool setHudVisibility(bool show) override; + bool isHudVisible() const override { return mHudEnabled; } void disallowMouse() override; void allowMouse() override; @@ -267,8 +268,8 @@ namespace MWGui enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; void staticMessageBox(std::string_view message) override; void removeStaticMessageBox() override; - void interactiveMessageBox( - std::string_view message, const std::vector& buttons = {}, bool block = false) override; + void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) override; int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed ///< (->MessageBoxmanager->InteractiveMessageBox) @@ -312,12 +313,10 @@ namespace MWGui void setEnemy(const MWWorld::Ptr& enemy) override; - int getMessagesCount() const override; + std::size_t getMessagesCount() const override; const Translation::Storage& getTranslationDataStorage() const override; - void onSoulgemDialogButtonPressed(int button); - bool getCursorVisible() override; /// Call when mouse cursor or buttons are used. @@ -363,6 +362,7 @@ namespace MWGui void toggleConsole() override; void toggleDebugWindow() override; void togglePostProcessorHud() override; + void toggleSettingsWindow() override; /// Cycle to next or previous spell void cycleSpell(bool next) override; @@ -560,7 +560,7 @@ namespace MWGui */ void onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result); - void onCursorChange(const std::string& name); + void onCursorChange(std::string_view name); void onKeyFocusChanged(MyGUI::Widget* widget); // Key pressed while playing a video @@ -568,8 +568,8 @@ namespace MWGui void sizeVideo(int screenWidth, int screenHeight); - void onClipboardChanged(const std::string& _type, const std::string& _data); - void onClipboardRequested(const std::string& _type, std::string& _data); + void onClipboardChanged(std::string_view _type, std::string_view _data); + void onClipboardRequested(std::string_view _type, std::string& _data); void createTextures(); void createCursors(); @@ -586,6 +586,9 @@ namespace MWGui void setCullMask(uint32_t mask) override; uint32_t getCullMask() override; + void setActiveMap(const MWWorld::Cell& cell); + ///< set the indices of the map texture that should be used + Files::ConfigurationManager& mCfgMgr; }; } diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index 3fac540f5e..154888c44c 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -4,7 +4,7 @@ #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -27,13 +27,11 @@ namespace MWInput { - ActionManager::ActionManager(BindingsManager* bindingsManager, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler) + ActionManager::ActionManager(BindingsManager* bindingsManager, osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler) : mBindingsManager(bindingsManager) , mViewer(std::move(viewer)) , mScreenCaptureHandler(std::move(screenCaptureHandler)) - , mScreenCaptureOperation(screenCaptureOperation) , mTimeIdle(0.f) { } @@ -118,7 +116,7 @@ namespace MWInput quickKey(10); break; case A_ToggleHUD: - windowManager->toggleHud(); + windowManager->setHudVisibility(!windowManager->isHudVisible()); break; case A_ToggleDebug: windowManager->toggleDebugWindow(); @@ -170,24 +168,8 @@ namespace MWInput void ActionManager::screenshot() { - const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); - bool regularScreenshot = settingStr.empty() || settingStr == "regular"; - - if (regularScreenshot) - { - mScreenCaptureHandler->setFramesToCapture(1); - mScreenCaptureHandler->captureNextFrame(*mViewer); - } - else - { - osg::ref_ptr screenshot(new osg::Image); - - if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) - { - (*mScreenCaptureOperation)(*(screenshot.get()), 0); - // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason - } - } + mScreenCaptureHandler->setFramesToCapture(1); + mScreenCaptureHandler->captureNextFrame(*mViewer); } void ActionManager::toggleMainMenu() diff --git a/apps/openmw/mwinput/actionmanager.hpp b/apps/openmw/mwinput/actionmanager.hpp index d78c6906bf..eb21f7ef79 100644 --- a/apps/openmw/mwinput/actionmanager.hpp +++ b/apps/openmw/mwinput/actionmanager.hpp @@ -17,9 +17,8 @@ namespace MWInput class ActionManager { public: - ActionManager(BindingsManager* bindingsManager, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler); + ActionManager(BindingsManager* bindingsManager, osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler); void update(float dt); @@ -48,7 +47,6 @@ namespace MWInput BindingsManager* mBindingsManager; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; - osgViewer::ScreenCaptureHandler::CaptureOperation* mScreenCaptureOperation; float mTimeIdle; }; diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index 29e66f7905..339ebf4276 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -1,5 +1,7 @@ #include "bindingsmanager.hpp" +#include + #include #include @@ -194,13 +196,22 @@ namespace MWInput BindingsManager::~BindingsManager() { + const std::string newFileName = Files::pathToUnicodeString(mUserFile) + ".new"; try { - mInputBinder->save(Files::pathToUnicodeString(mUserFile)); + if (mInputBinder->save(newFileName)) + { + std::filesystem::rename(Files::pathFromUnicodeString(newFileName), mUserFile); + Log(Debug::Info) << "Saved input bindings: " << mUserFile; + } + else + { + Log(Debug::Error) << "Failed to save input bindings to " << newFileName; + } } - catch (std::exception& e) + catch (const std::exception& e) { - Log(Debug::Error) << "Failed to save input bindings: " << e.what(); + Log(Debug::Error) << "Failed to save input bindings to " << newFileName << ": " << e.what(); } } @@ -381,10 +392,6 @@ namespace MWInput defaultButtonBindings[A_Inventory] = SDL_CONTROLLER_BUTTON_B; defaultButtonBindings[A_GameMenu] = SDL_CONTROLLER_BUTTON_START; defaultButtonBindings[A_QuickSave] = SDL_CONTROLLER_BUTTON_GUIDE; - defaultButtonBindings[A_MoveForward] = SDL_CONTROLLER_BUTTON_DPAD_UP; - defaultButtonBindings[A_MoveLeft] = SDL_CONTROLLER_BUTTON_DPAD_LEFT; - defaultButtonBindings[A_MoveBackward] = SDL_CONTROLLER_BUTTON_DPAD_DOWN; - defaultButtonBindings[A_MoveRight] = SDL_CONTROLLER_BUTTON_DPAD_RIGHT; std::map defaultAxisBindings; defaultAxisBindings[A_MoveForwardBackward] = SDL_CONTROLLER_AXIS_LEFTY; @@ -594,11 +601,12 @@ namespace MWInput } const std::initializer_list& BindingsManager::getActionControllerSorting() { - static const std::initializer_list actions{ A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Sneak, A_Activate, A_Use, - A_ToggleWeapon, A_ToggleSpell, A_AutoMove, A_Jump, A_Inventory, A_Journal, A_Rest, A_QuickSave, A_QuickLoad, - A_ToggleHUD, A_Screenshot, A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, - A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, A_CycleSpellLeft, A_CycleSpellRight, - A_CycleWeaponLeft, A_CycleWeaponRight }; + static const std::initializer_list actions{ A_MoveForward, A_MoveBackward, A_MoveLeft, A_MoveRight, + A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, A_ToggleSpell, A_AutoMove, + A_Jump, A_Inventory, A_Journal, A_Rest, A_QuickSave, A_QuickLoad, A_ToggleHUD, A_Screenshot, + A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, + A_QuickKey8, A_QuickKey9, A_QuickKey10, A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, + A_CycleWeaponRight }; return actions; } @@ -615,12 +623,12 @@ namespace MWInput return mInputBinder->detectingBindingState(); } - void BindingsManager::mousePressed(const SDL_MouseButtonEvent& arg, int deviceID) + void BindingsManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 deviceID) { mInputBinder->mousePressed(arg, deviceID); } - void BindingsManager::mouseReleased(const SDL_MouseButtonEvent& arg, int deviceID) + void BindingsManager::mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 deviceID) { mInputBinder->mouseReleased(arg, deviceID); } diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index a11baf74de..bee9e07cf7 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -47,8 +47,8 @@ namespace MWInput SDL_GameController* getControllerOrNull() const; - void mousePressed(const SDL_MouseButtonEvent& evt, int deviceID); - void mouseReleased(const SDL_MouseButtonEvent& arg, int deviceID); + void mousePressed(const SDL_MouseButtonEvent& evt, Uint8 deviceID); + void mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 deviceID); void mouseMoved(const SDLUtil::MouseMotionEvent& arg); void mouseWheelMoved(const SDL_MouseWheelEvent& arg); diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 7054f72c8f..0c0a3de57c 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -34,16 +34,27 @@ namespace MWInput { if (!controllerBindingsFile.empty()) { - SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(controllerBindingsFile).c_str()); + const int result + = SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(controllerBindingsFile).c_str()); + if (result < 0) + Log(Debug::Error) << "Failed to add game controller mappings from file \"" << controllerBindingsFile + << "\": " << SDL_GetError(); } if (!userControllerBindingsFile.empty()) { - SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(userControllerBindingsFile).c_str()); + const int result + = SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(userControllerBindingsFile).c_str()); + if (result < 0) + Log(Debug::Error) << "Failed to add game controller mappings from user file \"" + << userControllerBindingsFile << "\": " << SDL_GetError(); } // Open all presently connected sticks - int numSticks = SDL_NumJoysticks(); + const int numSticks = SDL_NumJoysticks(); + if (numSticks < 0) + Log(Debug::Error) << "Failed to get number of joysticks: " << SDL_GetError(); + for (int i = 0; i < numSticks; i++) { if (SDL_IsGameController(i)) @@ -52,11 +63,17 @@ namespace MWInput evt.which = i; static const int fakeDeviceID = 1; ControllerManager::controllerAdded(fakeDeviceID, evt); - Log(Debug::Info) << "Detected game controller: " << SDL_GameControllerNameForIndex(i); + if (const char* name = SDL_GameControllerNameForIndex(i)) + Log(Debug::Info) << "Detected game controller: " << name; + else + Log(Debug::Warning) << "Detected game controller without a name: " << SDL_GetError(); } else { - Log(Debug::Info) << "Detected unusable controller: " << SDL_JoystickNameForIndex(i); + if (const char* name = SDL_JoystickNameForIndex(i)) + Log(Debug::Info) << "Detected unusable controller: " << name; + else + Log(Debug::Warning) << "Detected unusable controller without a name: " << SDL_GetError(); } } @@ -77,7 +94,7 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - const float gamepadCursorSpeed = Settings::input().mEnableController; + const float gamepadCursorSpeed = Settings::input().mGamepadCursorSpeed; const float xMove = xAxis * dt * 1500.0f / uiScale * gamepadCursorSpeed; const float yMove = yAxis * dt * 1500.0f / uiScale * gamepadCursorSpeed; @@ -260,11 +277,11 @@ namespace MWInput key = MyGUI::KeyCode::Apostrophe; break; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - key = MyGUI::KeyCode::Period; - break; + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Period, 0, false); + return true; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - key = MyGUI::KeyCode::Slash; - break; + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Slash, 0, false); + return true; case SDL_CONTROLLER_BUTTON_LEFTSTICK: mGamepadGuiCursorEnabled = !mGamepadGuiCursorEnabled; MWBase::Environment::get().getWindowManager()->setCursorActive(mGamepadGuiCursorEnabled); @@ -336,8 +353,11 @@ namespace MWInput return; if (!SDL_GameControllerHasSensor(cntrl, SDL_SENSOR_GYRO)) return; - if (SDL_GameControllerSetSensorEnabled(cntrl, SDL_SENSOR_GYRO, SDL_TRUE) < 0) + if (const int result = SDL_GameControllerSetSensorEnabled(cntrl, SDL_SENSOR_GYRO, SDL_TRUE); result < 0) + { + Log(Debug::Error) << "Failed to enable game controller sensor: " << SDL_GetError(); return; + } mGyroAvailable = true; #endif } @@ -353,7 +373,11 @@ namespace MWInput #if SDL_VERSION_ATLEAST(2, 0, 14) SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); if (cntrl && mGyroAvailable) - SDL_GameControllerGetSensorData(cntrl, SDL_SENSOR_GYRO, gyro, 3); + { + const int result = SDL_GameControllerGetSensorData(cntrl, SDL_SENSOR_GYRO, gyro, 3); + if (result < 0) + Log(Debug::Error) << "Failed to get game controller sensor data: " << SDL_GetError(); + } #endif return std::array({ gyro[0], gyro[1], gyro[2] }); } diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index f9ca0a3432..328757a954 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -25,17 +25,14 @@ namespace MWInput { InputManager::InputManager(SDL_Window* window, osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - const std::filesystem::path& userFile, bool userFileExists, - const std::filesystem::path& userControllerBindingsFile, const std::filesystem::path& controllerBindingsFile, - bool grab) + osg::ref_ptr screenCaptureHandler, const std::filesystem::path& userFile, + bool userFileExists, const std::filesystem::path& userControllerBindingsFile, + const std::filesystem::path& controllerBindingsFile, bool grab) : mControlsDisabled(false) , mInputWrapper(std::make_unique(window, viewer, grab)) , mBindingsManager(std::make_unique(userFile, userFileExists)) , mControlSwitch(std::make_unique()) - , mActionManager(std::make_unique( - mBindingsManager.get(), screenCaptureOperation, viewer, screenCaptureHandler)) + , mActionManager(std::make_unique(mBindingsManager.get(), viewer, screenCaptureHandler)) , mKeyboardManager(std::make_unique(mBindingsManager.get())) , mMouseManager(std::make_unique(mBindingsManager.get(), mInputWrapper.get(), window)) , mControllerManager(std::make_unique( @@ -102,6 +99,11 @@ namespace MWInput mControllerManager->setGamepadGuiCursorEnabled(enabled); } + bool InputManager::isGamepadGuiCursorEnabled() + { + return mControllerManager->gamepadGuiCursorEnabled(); + } + void InputManager::changeInputMode(bool guiMode) { mControllerManager->setGuiCursorEnabled(guiMode); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index c5de579961..39a1133db5 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -49,10 +49,8 @@ namespace MWInput { public: InputManager(SDL_Window* window, osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - const std::filesystem::path& userFile, bool userFileExists, - const std::filesystem::path& userControllerBindingsFile, + osg::ref_ptr screenCaptureHandler, const std::filesystem::path& userFile, + bool userFileExists, const std::filesystem::path& userControllerBindingsFile, const std::filesystem::path& controllerBindingsFile, bool grab); ~InputManager() final; @@ -68,6 +66,7 @@ namespace MWInput void setDragDrop(bool dragDrop) override; void setGamepadGuiCursorEnabled(bool enabled) override; + bool isGamepadGuiCursorEnabled() override; void toggleControlSwitch(std::string_view sw, bool value) override; bool getControlSwitch(std::string_view sw) override; @@ -105,16 +104,6 @@ namespace MWInput bool controlsDisabled() override { return mControlsDisabled; } private: - void convertMousePosForMyGUI(int& x, int& y); - - void handleGuiArrowKey(int action); - - void quickKey(int index); - void showQuickKeysMenu(); - - void loadKeyDefaults(bool force = false); - void loadControllerDefaults(bool force = false); - bool mControlsDisabled; std::unique_ptr mInputWrapper; diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index 363a78fadd..eed95cf1c9 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -10,9 +10,12 @@ #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwgui/settingswindow.hpp" + #include "../mwworld/player.hpp" #include "actions.hpp" @@ -117,15 +120,22 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(!guiMode); mBindingsManager->mouseReleased(arg, id); } + + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::MouseButtonReleased, arg.button }); } void MouseManager::mouseWheelMoved(const SDL_MouseWheelEvent& arg) { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); if (mBindingsManager->isDetectingBindingState() || !input->controlsDisabled()) + { mBindingsManager->mouseWheelMoved(arg); + } input->setJoystickLastUsed(false); + MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::MouseWheel, + MWBase::LuaManager::InputEvent::WheelChange{ arg.x, arg.y } }); } void MouseManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 id) @@ -156,9 +166,12 @@ namespace MWInput // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible // Also do not trigger bindings when input controls are disabled, e.g. during save loading - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings - && !input->controlsDisabled()) + if (!MWBase::Environment::get().getWindowManager()->isSettingsWindowVisible() && !input->controlsDisabled()) + { mBindingsManager->mousePressed(arg, id); + } + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::MouseButtonPressed, arg.button }); } void MouseManager::updateCursorMode() @@ -205,14 +218,14 @@ namespace MWInput }; // Only actually turn player when we're not in vanity mode - bool controls = MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols"); - if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && controls) + bool playerLooking = MWBase::Environment::get().getInputManager()->getControlSwitch("playerlooking"); + if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && playerLooking) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.yaw(-rot[2]); player.pitch(-rot[0]); } - else if (!controls) + else if (!playerLooking) MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); MWBase::Environment::get().getInputManager()->resetIdleTime(); @@ -246,7 +259,8 @@ namespace MWInput void MouseManager::warpMouse() { - float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - mInputWrapper->warpMouse(static_cast(mGuiCursorX * uiScale), static_cast(mGuiCursorY * uiScale)); + float guiUiScale = Settings::gui().mScalingFactor; + mInputWrapper->warpMouse( + static_cast(mGuiCursorX * guiUiScale), static_cast(mGuiCursorY * guiUiScale)); } } diff --git a/apps/openmw/mwinput/sensormanager.cpp b/apps/openmw/mwinput/sensormanager.cpp index 32e48a008e..298006030a 100644 --- a/apps/openmw/mwinput/sensormanager.cpp +++ b/apps/openmw/mwinput/sensormanager.cpp @@ -42,8 +42,7 @@ namespace MWInput float angle = 0; - SDL_DisplayOrientation currentOrientation - = SDL_GetDisplayOrientation(Settings::Manager::getInt("screen", "Video")); + SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::video().mScreen); switch (currentOrientation) { case SDL_ORIENTATION_UNKNOWN: diff --git a/apps/openmw/mwlua/animationbindings.cpp b/apps/openmw/mwlua/animationbindings.cpp new file mode 100644 index 0000000000..b74a3e51c3 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -0,0 +1,336 @@ +#include "animationbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/character.hpp" + +#include "../mwworld/esmstore.hpp" + +#include "context.hpp" +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +namespace MWLua +{ + using BlendMask = MWRender::Animation::BlendMask; + using BoneGroup = MWRender::Animation::BoneGroup; + using Priority = MWMechanics::Priority; + using AnimationPriorities = MWRender::Animation::AnimPriority; + + MWWorld::Ptr getMutablePtrOrThrow(const ObjectVariant& variant) + { + if (variant.isLObject()) + throw std::runtime_error("Local scripts can only modify animations of the object they are attached to."); + + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + if (!ptr.getRefData().isEnabled()) + throw std::runtime_error("Can't use a disabled object"); + + return ptr; + } + + MWWorld::Ptr getPtrOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + MWRender::Animation* getMutableAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getMutablePtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + const MWRender::Animation* getConstAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getPtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + const MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + static AnimationPriorities getPriorityArgument(const sol::table& args) + { + auto asPriorityEnum = args.get>("priority"); + if (asPriorityEnum) + return asPriorityEnum.value(); + + auto asTable = args.get>("priority"); + if (asTable) + { + AnimationPriorities priorities = AnimationPriorities(Priority::Priority_Default); + for (const auto& entry : asTable.value()) + { + if (!entry.first.is() || !entry.second.is()) + throw std::runtime_error("Priority table must consist of BoneGroup-Priority pairs only"); + auto group = entry.first.as(); + auto priority = entry.second.as(); + if (group < 0 || group >= BoneGroup::Num_BoneGroups) + throw std::runtime_error("Invalid bonegroup: " + std::to_string(group)); + priorities[group] = priority; + } + + return priorities; + } + + return Priority::Priority_Default; + } + + sol::table initAnimationPackage(const Context& context) + { + auto view = context.sol(); + auto mechanics = MWBase::Environment::get().getMechanicsManager(); + auto world = MWBase::Environment::get().getWorld(); + + sol::table api(view, sol::create); + + api["PRIORITY"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "Default", MWMechanics::Priority::Priority_Default }, + { "WeaponLowerBody", MWMechanics::Priority::Priority_WeaponLowerBody }, + { "SneakIdleLowerBody", MWMechanics::Priority::Priority_SneakIdleLowerBody }, + { "SwimIdle", MWMechanics::Priority::Priority_SwimIdle }, + { "Jump", MWMechanics::Priority::Priority_Jump }, + { "Movement", MWMechanics::Priority::Priority_Movement }, + { "Hit", MWMechanics::Priority::Priority_Hit }, + { "Weapon", MWMechanics::Priority::Priority_Weapon }, + { "Block", MWMechanics::Priority::Priority_Block }, + { "Knockdown", MWMechanics::Priority::Priority_Knockdown }, + { "Torch", MWMechanics::Priority::Priority_Torch }, + { "Storm", MWMechanics::Priority::Priority_Storm }, + { "Death", MWMechanics::Priority::Priority_Death }, + { "Scripted", MWMechanics::Priority::Priority_Scripted }, + })); + + api["BLEND_MASK"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "LowerBody", BlendMask::BlendMask_LowerBody }, + { "Torso", BlendMask::BlendMask_Torso }, + { "LeftArm", BlendMask::BlendMask_LeftArm }, + { "RightArm", BlendMask::BlendMask_RightArm }, + { "UpperBody", BlendMask::BlendMask_UpperBody }, + { "All", BlendMask::BlendMask_All }, + })); + + api["BONE_GROUP"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "LowerBody", BoneGroup::BoneGroup_LowerBody }, + { "Torso", BoneGroup::BoneGroup_Torso }, + { "LeftArm", BoneGroup::BoneGroup_LeftArm }, + { "RightArm", BoneGroup::BoneGroup_RightArm }, + })); + + api["hasAnimation"] = [world](const sol::object& object) -> bool { + return world->getAnimation(getPtrOrThrow(ObjectVariant(object))) != nullptr; + }; + + // equivalent to MWScript's SkipAnim + api["skipAnimationThisFrame"] = [mechanics](const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + // This sets a flag that is only used during the update pass, so + // there's no need to queue + mechanics->skipAnimation(ptr); + }; + + api["getTextKeyTime"] = [](const sol::object& object, std::string_view key) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getTextKeyTime(key); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isPlaying"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isPlaying(groupname); + }; + api["getCurrentTime"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getCurrentTime(groupname); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isLoopingAnimation"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isLoopingAnimation(groupname); + }; + api["cancel"] = [](const sol::object& object, std::string_view groupname) { + return getMutableAnimationOrThrow(ObjectVariant(object))->disable(groupname); + }; + api["setLoopingEnabled"] = [](const sol::object& object, std::string_view groupname, bool enabled) { + return getMutableAnimationOrThrow(ObjectVariant(object))->setLoopingEnabled(groupname, enabled); + }; + // MWRender::Animation::getInfo can also return the current speed multiplier, but this is never used. + api["getCompletion"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float completion = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, &completion)) + return completion; + return sol::nullopt; + }; + api["getLoopCount"] = [](const sol::object& object, std::string groupname) -> sol::optional { + size_t loops = 0; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, nullptr, &loops)) + return loops; + return sol::nullopt; + }; + api["getSpeed"] = [](const sol::object& object, std::string groupname) -> sol::optional { + float speed = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, &speed, nullptr)) + return speed; + return sol::nullopt; + }; + api["setSpeed"] = [](const sol::object& object, std::string groupname, float speed) { + getMutableAnimationOrThrow(ObjectVariant(object))->adjustSpeedMult(groupname, speed); + }; + api["getActiveGroup"] = [](const sol::object& object, MWRender::BoneGroup boneGroup) -> std::string_view { + if (boneGroup < 0 || boneGroup >= BoneGroup::Num_BoneGroups) + throw std::runtime_error("Invalid bonegroup: " + std::to_string(boneGroup)); + return getConstAnimationOrThrow(ObjectVariant(object))->getActiveGroup(boneGroup); + }; + + // Clears out the animation queue, and cancel any animation currently playing from the queue + api["clearAnimationQueue"] = [mechanics](const sol::object& object, bool clearScripted) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->clearAnimationQueue(ptr, clearScripted); + }; + + // Extended variant of MWScript's PlayGroup and LoopGroup + api["playQueued"] = sol::overload( + [mechanics](const sol::object& object, const std::string& groupname, const sol::table& options) { + uint32_t numberOfLoops = options.get_or("loops", std::numeric_limits::max()); + float speed = options.get_or("speed", 1.f); + std::string startKey = options.get_or("startKey", "start"); + std::string stopKey = options.get_or("stopKey", "stop"); + bool forceLoop = options.get_or("forceLoop", false); + + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua(ptr, groupname, numberOfLoops, speed, startKey, stopKey, forceLoop); + }, + [mechanics](const sol::object& object, const std::string& groupname) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua( + ptr, groupname, std::numeric_limits::max(), 1, "start", "stop", false); + }); + + api["playBlended"] = [](const sol::object& object, std::string_view groupName, const sol::table& options) { + uint32_t loops = options.get_or("loops", 0u); + MWRender::Animation::AnimPriority priority = getPriorityArgument(options); + BlendMask blendMask = options.get_or("blendMask", BlendMask::BlendMask_All); + bool autoDisable = options.get_or("autoDisable", true); + float speed = options.get_or("speed", 1.0f); + std::string start = options.get_or("startKey", "start"); + std::string stop = options.get_or("stopKey", "stop"); + float startPoint = options.get_or("startPoint", 0.0f); + bool forceLoop = options.get_or("forceLoop", false); + + const std::string lowerGroup = Misc::StringUtils::lowerCase(groupName); + + auto animation = getMutableAnimationOrThrow(ObjectVariant(object)); + animation->play(lowerGroup, priority, blendMask, autoDisable, speed, start, stop, startPoint, loops, + forceLoop || animation->isLoopingAnimation(lowerGroup)); + }; + + api["hasGroup"] = [](const sol::object& object, std::string_view groupname) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->hasAnimation(groupname); + }; + + // Note: This checks the nodemap, and does not read the scene graph itself, and so should be thread safe. + api["hasBone"] = [](const sol::object& object, std::string_view bonename) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->getNode(bonename) != nullptr; + }; + + api["addVfx"] = [context]( + const sol::object& object, std::string_view model, sol::optional options) { + if (options) + { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = std::string(model), + effectId = options->get_or("vfxId", ""), loop = options->get_or("loop", false), + boneName = options->get_or("boneName", ""), + particleTexture = options->get_or("particleTextureOverride", "")] { + MWRender::Animation* anim = getMutableAnimationOrThrow(ObjectVariant(object)); + + anim->addEffect(model, effectId, loop, boneName, particleTexture); + }, + "addVfxAction"); + } + else + { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = std::string(model)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->addEffect(model, ""); + }, + "addVfxAction"); + } + }; + + api["removeVfx"] = [context](const sol::object& object, std::string_view effectId) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), effectId = std::string(effectId)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffect(effectId); + }, + "removeVfxAction"); + }; + + api["removeAllVfx"] = [context](const sol::object& object) { + context.mLuaManager->addAction( + [object = ObjectVariant(object)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffects(); + }, + "removeVfxAction"); + }; + + return LuaUtil::makeReadOnly(api); + } + + sol::table initWorldVfxBindings(const Context& context) + { + sol::table api(context.mLua->unsafeState(), sol::create); + auto world = MWBase::Environment::get().getWorld(); + + api["spawn"] + = [world, context](std::string_view model, const osg::Vec3f& worldPos, sol::optional options) { + if (options) + { + bool magicVfx = options->get_or("mwMagicVfx", true); + std::string texture = options->get_or("particleTextureOverride", ""); + float scale = options->get_or("scale", 1.f); + context.mLuaManager->addAction( + [world, model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale, + magicVfx]() { world->spawnEffect(model, texture, worldPos, scale, magicVfx); }, + "openmw.vfx.spawn"); + } + else + { + context.mLuaManager->addAction([world, model = VFS::Path::Normalized(model), + worldPos]() { world->spawnEffect(model, "", worldPos); }, + "openmw.vfx.spawn"); + } + }; + + return api; + } +} diff --git a/apps/openmw/mwlua/animationbindings.hpp b/apps/openmw/mwlua/animationbindings.hpp new file mode 100644 index 0000000000..aa89649a35 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.hpp @@ -0,0 +1,14 @@ +#ifndef MWLUA_ANIMATIONBINDINGS_H +#define MWLUA_ANIMATIONBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initAnimationPackage(const Context& context); + sol::table initWorldVfxBindings(const Context& context); +} + +#endif // MWLUA_ANIMATIONBINDINGS_H diff --git a/apps/openmw/mwlua/birthsignbindings.cpp b/apps/openmw/mwlua/birthsignbindings.cpp new file mode 100644 index 0000000000..218d05b804 --- /dev/null +++ b/apps/openmw/mwlua/birthsignbindings.cpp @@ -0,0 +1,47 @@ +#include "birthsignbindings.hpp" + +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "idcollectionbindings.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initBirthSignRecordBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table birthSigns(lua, sol::create); + addRecordFunctionBinding(birthSigns, context); + + auto signT = lua.new_usertype("ESM3_BirthSign"); + signT[sol::meta_function::to_string] = [](const ESM::BirthSign& rec) -> std::string { + return "ESM3_BirthSign[" + rec.mId.toDebugString() + "]"; + }; + signT["id"] = sol::readonly_property([](const ESM::BirthSign& rec) { return rec.mId.serializeText(); }); + signT["name"] = sol::readonly_property([](const ESM::BirthSign& rec) -> std::string_view { return rec.mName; }); + signT["description"] + = sol::readonly_property([](const ESM::BirthSign& rec) -> std::string_view { return rec.mDescription; }); + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + signT["texture"] = sol::readonly_property([vfs](const ESM::BirthSign& rec) -> std::string { + return Misc::ResourceHelpers::correctTexturePath(rec.mTexture, vfs); + }); + signT["spells"] = sol::readonly_property([lua](const ESM::BirthSign& rec) -> sol::table { + return createReadOnlyRefIdTable(lua, rec.mPowers.mList); + }); + + return LuaUtil::makeReadOnly(birthSigns); + } +} diff --git a/apps/openmw/mwlua/birthsignbindings.hpp b/apps/openmw/mwlua/birthsignbindings.hpp new file mode 100644 index 0000000000..bf41707d47 --- /dev/null +++ b/apps/openmw/mwlua/birthsignbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_BIRTHSIGNBINDINGS_H +#define MWLUA_BIRTHSIGNBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initBirthSignRecordBindings(const Context& context); +} + +#endif // MWLUA_BIRTHSIGNBINDINGS_H diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp index bbdba00ee2..d64110ea98 100644 --- a/apps/openmw/mwlua/camerabindings.cpp +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -1,3 +1,4 @@ +#include "camerabindings.hpp" #include #include @@ -13,7 +14,7 @@ namespace MWLua using CameraMode = MWRender::Camera::Mode; - sol::table initCameraPackage(sol::state_view& lua) + sol::table initCameraPackage(sol::state_view lua) { MWRender::Camera* camera = MWBase::Environment::get().getWorld()->getCamera(); MWRender::RenderingManager* renderingManager = MWBase::Environment::get().getWorld()->getRenderingManager(); @@ -94,8 +95,8 @@ namespace MWLua api["getViewTransform"] = [camera]() { return LuaUtil::TransformM{ camera->getViewMatrix() }; }; api["viewportToWorldVector"] = [camera, renderingManager](osg::Vec2f pos) -> osg::Vec3f { - double width = Settings::Manager::getInt("resolution x", "Video"); - double height = Settings::Manager::getInt("resolution y", "Video"); + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; double aspect = (height == 0.0) ? 1.0 : width / height; double fovTan = std::tan(osg::DegreesToRadians(renderingManager->getFieldOfView()) / 2); osg::Matrixf invertedViewMatrix; @@ -106,8 +107,8 @@ namespace MWLua }; api["worldToViewportVector"] = [camera](osg::Vec3f pos) { - double width = Settings::Manager::getInt("resolution x", "Video"); - double height = Settings::Manager::getInt("resolution y", "Video"); + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; osg::Matrix windowMatrix = osg::Matrix::translate(1.0, 1.0, 1.0) * osg::Matrix::scale(0.5 * width, 0.5 * height, 0.5); diff --git a/apps/openmw/mwlua/camerabindings.hpp b/apps/openmw/mwlua/camerabindings.hpp index be468495e1..001ef32167 100644 --- a/apps/openmw/mwlua/camerabindings.hpp +++ b/apps/openmw/mwlua/camerabindings.hpp @@ -5,7 +5,7 @@ namespace MWLua { - sol::table initCameraPackage(sol::state_view& lua); + sol::table initCameraPackage(sol::state_view lua); } #endif // MWLUA_CAMERABINDINGS_H diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index 48c7141ab8..933dba3fda 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -30,15 +31,21 @@ #include #include #include +#include #include +#include #include #include #include +#include #include +#include #include #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/worldmodel.hpp" @@ -62,7 +69,8 @@ namespace MWLua template static void initCellBindings(const std::string& prefix, const Context& context) { - sol::usertype cellT = context.mLua->sol().new_usertype(prefix + "Cell"); + auto view = context.sol(); + sol::usertype cellT = view.new_usertype(prefix + "Cell"); cellT[sol::meta_function::equal_to] = [](const CellT& a, const CellT& b) { return a.mStore == b.mStore; }; cellT[sol::meta_function::to_string] = [](const CellT& c) { @@ -77,6 +85,8 @@ namespace MWLua }; cellT["name"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getNameId(); }); + cellT["id"] + = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getId().serializeText(); }); cellT["region"] = sol::readonly_property( [](const CellT& c) -> std::string { return c.mStore->getCell()->getRegion().serializeText(); }); cellT["worldSpaceId"] = sol::readonly_property( @@ -109,15 +119,21 @@ namespace MWLua return cell == c.mStore || (cell->getCell()->getWorldSpace() == c.mStore->getCell()->getWorldSpace()); }; + cellT["waterLevel"] = sol::readonly_property([](const CellT& c) -> sol::optional { + if (c.mStore->getCell()->hasWater()) + return c.mStore->getWaterLevel(); + else + return sol::nullopt; + }); + if constexpr (std::is_same_v) { // only for global scripts - cellT["getAll"] = [ids = getPackageToTypeTable(context.mLua->sol())]( - const CellT& cell, sol::optional type) { + cellT["getAll"] = [ids = getPackageToTypeTable(view)](const CellT& cell, sol::optional type) { if (cell.mStore->getState() != MWWorld::CellStore::State_Loaded) cell.mStore->load(); ObjectIdList res = std::make_shared>(); auto visitor = [&](const MWWorld::Ptr& ptr) { - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) return true; MWBase::Environment::get().getWorldModel()->registerPtr(ptr); if (getLiveCellRefType(ptr.mRef) == ptr.getType()) @@ -198,6 +214,9 @@ namespace MWLua case ESM::REC_STAT: cell.mStore->template forEachType(visitor); break; + case ESM::REC_LEVC: + cell.mStore->template forEachType(visitor); + break; case ESM::REC_ACTI4: cell.mStore->template forEachType(visitor); @@ -220,9 +239,15 @@ namespace MWLua case ESM::REC_DOOR4: cell.mStore->template forEachType(visitor); break; + case ESM::REC_FLOR4: + cell.mStore->template forEachType(visitor); + break; case ESM::REC_FURN4: cell.mStore->template forEachType(visitor); break; + case ESM::REC_IMOD4: + cell.mStore->template forEachType(visitor); + break; case ESM::REC_INGR4: cell.mStore->template forEachType(visitor); break; @@ -232,9 +257,15 @@ namespace MWLua case ESM::REC_MISC4: cell.mStore->template forEachType(visitor); break; + case ESM::REC_MSTT4: + cell.mStore->template forEachType(visitor); + break; case ESM::REC_ALCH4: cell.mStore->template forEachType(visitor); break; + case ESM::REC_SCOL4: + cell.mStore->template forEachType(visitor); + break; case ESM::REC_STAT4: cell.mStore->template forEachType(visitor); break; @@ -252,7 +283,7 @@ namespace MWLua if (!ok) throw std::runtime_error( std::string("Incorrect type argument in cell:getAll: " + LuaUtil::toString(*type))); - return GObjectList{ res }; + return GObjectList{ std::move(res) }; }; } } diff --git a/apps/openmw/mwlua/classbindings.cpp b/apps/openmw/mwlua/classbindings.cpp new file mode 100644 index 0000000000..c94631fb3c --- /dev/null +++ b/apps/openmw/mwlua/classbindings.cpp @@ -0,0 +1,53 @@ +#include "classbindings.hpp" + +#include +#include + +#include "idcollectionbindings.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + sol::table initClassRecordBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table classes(lua, sol::create); + addRecordFunctionBinding(classes, context); + + auto classT = lua.new_usertype("ESM3_Class"); + classT[sol::meta_function::to_string] + = [](const ESM::Class& rec) -> std::string { return "ESM3_Class[" + rec.mId.toDebugString() + "]"; }; + classT["id"] = sol::readonly_property([](const ESM::Class& rec) { return rec.mId.serializeText(); }); + classT["name"] = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { return rec.mName; }); + classT["description"] + = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { return rec.mDescription; }); + + classT["attributes"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + return createReadOnlyRefIdTable(lua, rec.mData.mAttribute, ESM::Attribute::indexToRefId); + }); + classT["majorSkills"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + return createReadOnlyRefIdTable( + lua, rec.mData.mSkills, [](const auto& pair) { return ESM::Skill::indexToRefId(pair[1]); }); + }); + classT["minorSkills"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + return createReadOnlyRefIdTable( + lua, rec.mData.mSkills, [](const auto& pair) { return ESM::Skill::indexToRefId(pair[0]); }); + }); + + classT["specialization"] = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { + return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); + }); + classT["isPlayable"] + = sol::readonly_property([](const ESM::Class& rec) -> bool { return rec.mData.mIsPlayable; }); + return LuaUtil::makeReadOnly(classes); + } +} diff --git a/apps/openmw/mwlua/classbindings.hpp b/apps/openmw/mwlua/classbindings.hpp new file mode 100644 index 0000000000..1acb0a9ad3 --- /dev/null +++ b/apps/openmw/mwlua/classbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_CLASSBINDINGS_H +#define MWLUA_CLASSBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initClassRecordBindings(const Context& context); +} + +#endif // MWLUA_CLASSBINDINGS_H diff --git a/apps/openmw/mwlua/context.hpp b/apps/openmw/mwlua/context.hpp index 68b46164d6..363507bdc9 100644 --- a/apps/openmw/mwlua/context.hpp +++ b/apps/openmw/mwlua/context.hpp @@ -1,6 +1,8 @@ #ifndef MWLUA_CONTEXT_H #define MWLUA_CONTEXT_H +#include + namespace LuaUtil { class LuaState; @@ -15,12 +17,90 @@ namespace MWLua struct Context { - bool mIsGlobal; + enum Type + { + Menu, + Global, + Local, + }; + Type mType; LuaManager* mLuaManager; LuaUtil::LuaState* mLua; LuaUtil::UserdataSerializer* mSerializer; ObjectLists* mObjectLists; LuaEvents* mLuaEvents; + + std::string_view typeName() const + { + switch (mType) + { + case Menu: + return "menu"; + case Global: + return "global"; + case Local: + return "local"; + default: + throw std::domain_error("Unhandled context type"); + } + } + + template + sol::object getCachedPackage(std::string_view first, const Str&... str) const + { + sol::object package = sol()[first]; + if constexpr (sizeof...(str) == 0) + return package; + else + return LuaUtil::getFieldOrNil(package, str...); + } + + template + const sol::object& setCachedPackage(const sol::object& value, std::string_view first, const Str&... str) const + { + sol::state_view lua = sol(); + if constexpr (sizeof...(str) == 0) + lua[first] = value; + else + { + if (lua[first] == sol::nil) + lua[first] = sol::table(lua, sol::create); + sol::table table = lua[first]; + LuaUtil::setDeepField(table, value, str...); + } + return value; + } + + sol::object getTypePackage(std::string_view key) const { return getCachedPackage(key, typeName()); } + + const sol::object& setTypePackage(const sol::object& value, std::string_view key) const + { + return setCachedPackage(value, key, typeName()); + } + + template + sol::object cachePackage(std::string_view key, Factory factory) const + { + sol::object cached = getCachedPackage(key); + if (cached != sol::nil) + return cached; + else + return setCachedPackage(factory(), key); + } + + bool initializeOnce(std::string_view key) const + { + auto view = sol(); + sol::object flag = view[key]; + view[key] = sol::make_object(view, true); + return flag == sol::nil; + } + + sol::state_view sol() const + { + // Bindings are initialized in a safe context + return mLua->unsafeState(); + } }; } diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp new file mode 100644 index 0000000000..445bcdd617 --- /dev/null +++ b/apps/openmw/mwlua/corebindings.cpp @@ -0,0 +1,154 @@ +#include "corebindings.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" + +#include "dialoguebindings.hpp" +#include "factionbindings.hpp" +#include "luaevents.hpp" +#include "magicbindings.hpp" +#include "soundbindings.hpp" +#include "stats.hpp" + +namespace MWLua +{ + static sol::table initContentFilesBindings(sol::state_view& lua) + { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + sol::table list(lua, sol::create); + for (size_t i = 0; i < contentList.size(); ++i) + list[LuaUtil::toLuaIndex(i)] = Misc::StringUtils::lowerCase(contentList[i]); + sol::table res(lua, sol::create); + res["list"] = LuaUtil::makeReadOnly(list); + res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return LuaUtil::toLuaIndex(i); + return sol::nullopt; + }; + res["has"] = [&contentList](std::string_view contentFile) -> bool { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return true; + return false; + }; + return LuaUtil::makeReadOnly(res); + } + + void addCoreTimeBindings(sol::table& api, const Context& context) + { + MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); + + api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); }; + api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); }; + api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); }; + api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); }; + api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); }; + api["getRealTime"] = []() { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + }; + + if (context.mType != Context::Global) + api["getRealFrameDuration"] = []() { return MWBase::Environment::get().getFrameDuration(); }; + } + + sol::table initCorePackage(const Context& context) + { + auto lua = context.sol(); + sol::object cached = context.getTypePackage("openmw_core"); + if (cached != sol::nil) + return cached; + + sol::table api(lua, sol::create); + api["API_REVISION"] = Version::getLuaApiRevision(); // specified in CMakeLists.txt + api["quit"] = [lua = context.mLua]() { + Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); + MWBase::Environment::get().getStateManager()->requestQuit(); + }; + api["contentFiles"] = initContentFilesBindings(lua); + api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText(); + throw std::runtime_error("Content file not found: " + std::string(contentFile)); + }; + addCoreTimeBindings(api, context); + + api["magic"] + = context.cachePackage("openmw_core_magic", [context]() { return initCoreMagicBindings(context); }); + + api["stats"] + = context.cachePackage("openmw_core_stats", [context]() { return initCoreStatsBindings(context); }); + + api["factions"] + = context.cachePackage("openmw_core_factions", [context]() { return initCoreFactionBindings(context); }); + api["dialogue"] + = context.cachePackage("openmw_core_dialogue", [context]() { return initCoreDialogueBindings(context); }); + api["l10n"] = context.cachePackage("openmw_core_l10n", + [lua]() { return LuaUtil::initL10nLoader(lua, MWBase::Environment::get().getL10nManager()); }); + const MWWorld::Store* gmstStore + = &MWBase::Environment::get().getESMStore()->get(); + api["getGMST"] = [lua, gmstStore](const std::string& setting) -> sol::object { + const ESM::GameSetting* gmst = gmstStore->search(setting); + if (gmst == nullptr) + return sol::nil; + const ESM::Variant& value = gmst->mValue; + switch (value.getType()) + { + case ESM::VT_Float: + return sol::make_object(lua, value.getFloat()); + case ESM::VT_Short: + case ESM::VT_Long: + case ESM::VT_Int: + return sol::make_object(lua, value.getInteger()); + case ESM::VT_String: + return sol::make_object(lua, value.getString()); + case ESM::VT_Unknown: + case ESM::VT_None: + break; + } + return sol::nil; + }; + + if (context.mType != Context::Menu) + { + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { + context.mLuaEvents->addGlobalEvent( + { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); + }; + api["sound"] + = context.cachePackage("openmw_core_sound", [context]() { return initCoreSoundBindings(context); }); + } + else + { + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + { + throw std::logic_error("Can't send global events when no game is loaded"); + } + context.mLuaEvents->addGlobalEvent( + { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); + }; + } + + sol::table readOnly = LuaUtil::makeReadOnly(api); + return context.setTypePackage(readOnly, "openmw_core"); + } +} diff --git a/apps/openmw/mwlua/corebindings.hpp b/apps/openmw/mwlua/corebindings.hpp new file mode 100644 index 0000000000..ef385ca993 --- /dev/null +++ b/apps/openmw/mwlua/corebindings.hpp @@ -0,0 +1,15 @@ +#ifndef MWLUA_COREBINDINGS_H +#define MWLUA_COREBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + void addCoreTimeBindings(sol::table& api, const Context& context); + + sol::table initCorePackage(const Context&); +} + +#endif // MWLUA_COREBINDINGS_H diff --git a/apps/openmw/mwlua/debugbindings.cpp b/apps/openmw/mwlua/debugbindings.cpp index 7f64188ff5..dcb77580ce 100644 --- a/apps/openmw/mwlua/debugbindings.cpp +++ b/apps/openmw/mwlua/debugbindings.cpp @@ -1,9 +1,12 @@ #include "debugbindings.hpp" + #include "context.hpp" #include "luamanagerimp.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" + #include "../mwrender/postprocessor.hpp" #include "../mwrender/renderingmanager.hpp" @@ -17,19 +20,21 @@ namespace MWLua { sol::table initDebugPackage(const Context& context) { - sol::table api = context.mLua->newTable(); + auto view = context.sol(); + sol::table api(view, sol::create); api["RENDER_MODE"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "CollisionDebug", MWRender::Render_CollisionDebug }, - { "Wireframe", MWRender::Render_Wireframe }, - { "Pathgrid", MWRender::Render_Pathgrid }, - { "Water", MWRender::Render_Water }, - { "Scene", MWRender::Render_Scene }, - { "NavMesh", MWRender::Render_NavMesh }, - { "ActorsPaths", MWRender::Render_ActorsPaths }, - { "RecastMesh", MWRender::Render_RecastMesh }, - })); + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "CollisionDebug", MWRender::Render_CollisionDebug }, + { "Wireframe", MWRender::Render_Wireframe }, + { "Pathgrid", MWRender::Render_Pathgrid }, + { "Water", MWRender::Render_Water }, + { "Scene", MWRender::Render_Scene }, + { "NavMesh", MWRender::Render_NavMesh }, + { "ActorsPaths", MWRender::Render_ActorsPaths }, + { "RecastMesh", MWRender::Render_RecastMesh }, + })); api["toggleRenderMode"] = [context](MWRender::RenderMode value) { context.mLuaManager->addAction([value] { MWBase::Environment::get().getWorld()->toggleRenderMode(value); }); @@ -38,19 +43,28 @@ namespace MWLua api["toggleGodMode"] = []() { MWBase::Environment::get().getWorld()->toggleGodMode(); }; api["isGodMode"] = []() { return MWBase::Environment::get().getWorld()->getGodModeState(); }; + api["toggleAI"] = []() { MWBase::Environment::get().getMechanicsManager()->toggleAI(); }; + api["isAIEnabled"] = []() { return MWBase::Environment::get().getMechanicsManager()->isAIActive(); }; + api["toggleCollision"] = []() { MWBase::Environment::get().getWorld()->toggleCollisionMode(); }; api["isCollisionEnabled"] = []() { auto world = MWBase::Environment::get().getWorld(); return world->isActorCollisionEnabled(world->getPlayerPtr()); }; - api["NAV_MESH_RENDER_MODE"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "AreaType", MWRender::NavMeshMode::AreaType }, - { "UpdateFrequency", MWRender::NavMeshMode::UpdateFrequency }, - })); + api["toggleMWScript"] = []() { MWBase::Environment::get().getWorld()->toggleScripts(); }; + api["isMWScriptEnabled"] = []() { return MWBase::Environment::get().getWorld()->getScriptsEnabled(); }; - api["setNavMeshRenderMode"] = [context](MWRender::NavMeshMode value) { + api["reloadLua"] = []() { MWBase::Environment::get().getLuaManager()->reloadAllScripts(); }; + + api["NAV_MESH_RENDER_MODE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "AreaType", Settings::NavMeshRenderMode::AreaType }, + { "UpdateFrequency", Settings::NavMeshRenderMode::UpdateFrequency }, + })); + + api["setNavMeshRenderMode"] = [context](Settings::NavMeshRenderMode value) { context.mLuaManager->addAction( [value] { MWBase::Environment::get().getWorld()->getRenderingManager()->setNavMeshMode(value); }); }; diff --git a/apps/openmw/mwlua/dialoguebindings.cpp b/apps/openmw/mwlua/dialoguebindings.cpp new file mode 100644 index 0000000000..d0ca799cb6 --- /dev/null +++ b/apps/openmw/mwlua/dialoguebindings.cpp @@ -0,0 +1,343 @@ +#include "dialoguebindings.hpp" + +#include "context.hpp" + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/store.hpp" + +#include +#include +#include +#include +#include + +namespace +{ + std::vector makeIndex(const MWWorld::Store& store, ESM::Dialogue::Type type) + { + std::vector result; + for (const ESM::Dialogue& v : store) + if (v.mType == type) + result.push_back(&v); + return result; + } + + template + class FilteredDialogueStore + { + const MWWorld::Store& mDialogueStore; + std::vector mIndex; + + public: + explicit FilteredDialogueStore(const MWWorld::Store& store) + : mDialogueStore(store) + , mIndex{ makeIndex(store, type) } + { + } + + const ESM::Dialogue* search(const ESM::RefId& id) const + { + const ESM::Dialogue* dialogue = mDialogueStore.search(id); + if (dialogue != nullptr && dialogue->mType == type) + return dialogue; + return nullptr; + } + + const ESM::Dialogue* at(std::size_t index) const + { + if (index >= mIndex.size()) + return nullptr; + return mIndex[index]; + } + + std::size_t getSize() const { return mIndex.size(); } + }; + + template + void prepareBindingsForDialogueRecordStores(sol::table& table, const MWLua::Context& context) + { + using StoreT = FilteredDialogueStore; + + sol::state_view lua = context.sol(); + sol::usertype storeBindingsClass + = lua.new_usertype("ESM3_Dialogue_Type" + std::to_string(filter) + " Store"); + storeBindingsClass[sol::meta_function::to_string] = [](const StoreT& store) { + return "{" + std::to_string(store.getSize()) + " ESM3_Dialogue_Type" + std::to_string(filter) + " records}"; + }; + storeBindingsClass[sol::meta_function::length] = [](const StoreT& store) { return store.getSize(); }; + storeBindingsClass[sol::meta_function::index] = sol::overload( + [](const StoreT& store, size_t index) -> const ESM::Dialogue* { + if (index == 0) + { + return nullptr; + } + return store.at(LuaUtil::fromLuaIndex(index)); + }, + [](const StoreT& store, std::string_view id) -> const ESM::Dialogue* { + return store.search(ESM::RefId::deserializeText(id)); + }); + storeBindingsClass[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + storeBindingsClass[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + + table["records"] = StoreT{ MWBase::Environment::get().getESMStore()->get() }; + } + + struct DialogueInfos + { + const ESM::Dialogue& parentDialogueRecord; + }; + + void prepareBindingsForDialogueRecord(sol::state_view& lua) + { + auto recordBindingsClass = lua.new_usertype("ESM3_Dialogue"); + recordBindingsClass[sol::meta_function::to_string] + = [](const ESM::Dialogue& rec) { return "ESM3_Dialogue[" + rec.mId.toDebugString() + "]"; }; + recordBindingsClass["id"] + = sol::readonly_property([](const ESM::Dialogue& rec) { return rec.mId.serializeText(); }); + recordBindingsClass["name"] + = sol::readonly_property([](const ESM::Dialogue& rec) -> std::string_view { return rec.mStringId; }); + recordBindingsClass["questName"] + = sol::readonly_property([](const ESM::Dialogue& rec) -> sol::optional { + if (rec.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + for (const auto& mwDialogueInfo : rec.mInfo) + { + if (mwDialogueInfo.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Name) + { + return sol::optional(mwDialogueInfo.mResponse); + } + } + return sol::nullopt; + }); + recordBindingsClass["infos"] + = sol::readonly_property([](const ESM::Dialogue& rec) { return DialogueInfos{ rec }; }); + } + + void prepareBindingsForDialogueRecordInfoList(sol::state_view& lua) + { + auto recordInfosBindingsClass = lua.new_usertype("ESM3_Dialogue_Infos"); + recordInfosBindingsClass[sol::meta_function::to_string] = [](const DialogueInfos& store) { + const ESM::Dialogue& dialogueRecord = store.parentDialogueRecord; + return "{" + std::to_string(dialogueRecord.mInfo.size()) + " ESM3_Dialogue[" + + dialogueRecord.mId.toDebugString() + "] info elements}"; + }; + recordInfosBindingsClass[sol::meta_function::length] + = [](const DialogueInfos& store) { return store.parentDialogueRecord.mInfo.size(); }; + recordInfosBindingsClass[sol::meta_function::index] + = [](const DialogueInfos& store, size_t index) -> const ESM::DialInfo* { + const ESM::Dialogue& dialogueRecord = store.parentDialogueRecord; + if (index == 0 || index > dialogueRecord.mInfo.size()) + { + return nullptr; + } + ESM::Dialogue::InfoContainer::const_iterator iter{ dialogueRecord.mInfo.cbegin() }; + std::advance(iter, LuaUtil::fromLuaIndex(index)); + return &(*iter); + }; + recordInfosBindingsClass[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + recordInfosBindingsClass[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + } + + void prepareBindingsForDialogueRecordInfoListElement(sol::state_view& lua) + { + auto recordInfoBindingsClass = lua.new_usertype("ESM3_Dialogue_Info"); + + recordInfoBindingsClass[sol::meta_function::to_string] + = [](const ESM::DialInfo& rec) { return "ESM3_Dialogue_Info[" + rec.mId.toDebugString() + "]"; }; + recordInfoBindingsClass["id"] + = sol::readonly_property([](const ESM::DialInfo& rec) { return rec.mId.serializeText(); }); + recordInfoBindingsClass["text"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> std::string_view { return rec.mResponse; }); + recordInfoBindingsClass["questStage"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return rec.mData.mJournalIndex; + }); + recordInfoBindingsClass["isQuestFinished"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return (rec.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Finished); + }); + recordInfoBindingsClass["isQuestRestart"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return (rec.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Restart); + }); + recordInfoBindingsClass["isQuestName"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return (rec.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Name); + }); + recordInfoBindingsClass["filterActorId"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mActor.empty()) + { + return sol::nullopt; + } + return rec.mActor.serializeText(); + }); + recordInfoBindingsClass["filterActorRace"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mRace.empty()) + { + return sol::nullopt; + } + return rec.mRace.serializeText(); + }); + recordInfoBindingsClass["filterActorClass"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mClass.empty()) + { + return sol::nullopt; + } + return rec.mClass.serializeText(); + }); + recordInfoBindingsClass["filterActorFaction"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mFaction.empty()) + { + return sol::nullopt; + } + if (rec.mFactionLess) + { + return sol::optional(""); + } + return rec.mFaction.serializeText(); + }); + recordInfoBindingsClass["filterActorFactionRank"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mData.mRank == -1) + { + return sol::nullopt; + } + return LuaUtil::toLuaIndex(rec.mData.mRank); + }); + recordInfoBindingsClass["filterPlayerCell"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mCell.empty()) + { + return sol::nullopt; + } + return rec.mCell.serializeText(); + }); + recordInfoBindingsClass["filterActorDisposition"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return rec.mData.mDisposition; + }); + recordInfoBindingsClass["filterActorGender"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mData.mGender == -1) + { + return sol::nullopt; + } + return sol::optional(rec.mData.mGender == 0 ? "male" : "female"); + }); + recordInfoBindingsClass["filterPlayerFaction"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mPcFaction.empty()) + { + return sol::nullopt; + } + return rec.mPcFaction.serializeText(); + }); + recordInfoBindingsClass["filterPlayerFactionRank"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mData.mPCrank == -1) + { + return sol::nullopt; + } + return LuaUtil::toLuaIndex(rec.mData.mPCrank); + }); + recordInfoBindingsClass["sound"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mSound.empty()) + { + return sol::nullopt; + } + return Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(rec.mSound)).value(); + }); + recordInfoBindingsClass["resultScript"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mResultScript.empty()) + { + return sol::nullopt; + } + return sol::optional(rec.mResultScript); + }); + } + + void prepareBindingsForDialogueRecords(sol::state_view& lua) + { + prepareBindingsForDialogueRecord(lua); + prepareBindingsForDialogueRecordInfoList(lua); + prepareBindingsForDialogueRecordInfoListElement(lua); + } +} + +namespace sol +{ + template + struct is_automagical> : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initCoreDialogueBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + sol::table journalTable(lua, sol::create); + sol::table topicTable(lua, sol::create); + sol::table greetingTable(lua, sol::create); + sol::table persuasionTable(lua, sol::create); + sol::table voiceTable(lua, sol::create); + prepareBindingsForDialogueRecordStores(journalTable, context); + prepareBindingsForDialogueRecordStores(topicTable, context); + prepareBindingsForDialogueRecordStores(greetingTable, context); + prepareBindingsForDialogueRecordStores(persuasionTable, context); + prepareBindingsForDialogueRecordStores(voiceTable, context); + api["journal"] = LuaUtil::makeStrictReadOnly(journalTable); + api["topic"] = LuaUtil::makeStrictReadOnly(topicTable); + api["greeting"] = LuaUtil::makeStrictReadOnly(greetingTable); + api["persuasion"] = LuaUtil::makeStrictReadOnly(persuasionTable); + api["voice"] = LuaUtil::makeStrictReadOnly(voiceTable); + + prepareBindingsForDialogueRecords(lua); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/dialoguebindings.hpp b/apps/openmw/mwlua/dialoguebindings.hpp new file mode 100644 index 0000000000..a4ee242427 --- /dev/null +++ b/apps/openmw/mwlua/dialoguebindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_DIALOGUEBINDINGS_H +#define MWLUA_DIALOGUEBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initCoreDialogueBindings(const Context& context); +} + +#endif // MWLUA_DIALOGUEBINDINGS_H diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp index 569eb22bcf..6c652bccba 100644 --- a/apps/openmw/mwlua/engineevents.cpp +++ b/apps/openmw/mwlua/engineevents.cpp @@ -65,6 +65,15 @@ namespace MWLua scripts->onActivated(LObject(actor)); } + void operator()(const OnUseItem& event) const + { + MWWorld::Ptr obj = getPtr(event.mObject); + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty() || obj.isEmpty()) + return; + mGlobalScripts.onUseItem(GObject(obj), GObject(actor), event.mForce); + } + void operator()(const OnConsume& event) const { MWWorld::Ptr actor = getPtr(event.mActor); @@ -77,8 +86,35 @@ namespace MWLua void operator()(const OnNewExterior& event) const { mGlobalScripts.onNewExterior(GCell{ &event.mCell }); } + void operator()(const OnAnimationTextKey& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onAnimationTextKey(event.mGroupname, event.mKey); + } + + void operator()(const OnSkillUse& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onSkillUse(event.mSkill, event.useType, event.scale); + } + + void operator()(const OnSkillLevelUp& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onSkillLevelUp(event.mSkill, event.mSource); + } + private: - MWWorld::Ptr getPtr(const ESM::RefNum& id) const + MWWorld::Ptr getPtr(ESM::RefNum id) const { MWWorld::Ptr res = mWorldModel->getPtr(id); if (res.isEmpty() && Settings::lua().mLuaDebug) @@ -94,7 +130,7 @@ namespace MWLua return ptr.getRefData().getLuaScripts(); } - LocalScripts* getLocalScripts(const ESM::RefNum& id) const { return getLocalScripts(getPtr(id)); } + LocalScripts* getLocalScripts(ESM::RefNum id) const { return getLocalScripts(getPtr(id)); } GlobalScripts& mGlobalScripts; MWWorld::WorldModel* mWorldModel = MWBase::Environment::get().getWorldModel(); diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp index ac854abd4a..fb9183eb7c 100644 --- a/apps/openmw/mwlua/engineevents.hpp +++ b/apps/openmw/mwlua/engineevents.hpp @@ -36,6 +36,12 @@ namespace MWLua ESM::RefNum mActor; ESM::RefNum mObject; }; + struct OnUseItem + { + ESM::RefNum mActor; + ESM::RefNum mObject; + bool mForce; + }; struct OnConsume { ESM::RefNum mActor; @@ -45,7 +51,27 @@ namespace MWLua { MWWorld::CellStore& mCell; }; - using Event = std::variant; + struct OnAnimationTextKey + { + ESM::RefNum mActor; + std::string mGroupname; + std::string mKey; + }; + struct OnSkillUse + { + ESM::RefNum mActor; + std::string mSkill; + int useType; + float scale; + }; + struct OnSkillLevelUp + { + ESM::RefNum mActor; + std::string mSkill; + std::string mSource; + }; + using Event = std::variant; void clear() { mQueue.clear(); } void addToQueue(Event e) { mQueue.push_back(std::move(e)); } diff --git a/apps/openmw/mwlua/factionbindings.cpp b/apps/openmw/mwlua/factionbindings.cpp new file mode 100644 index 0000000000..45234827de --- /dev/null +++ b/apps/openmw/mwlua/factionbindings.cpp @@ -0,0 +1,113 @@ +#include "factionbindings.hpp" +#include "recordstore.hpp" + +#include +#include +#include + +#include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/environment.hpp" + +#include "../mwworld/store.hpp" + +#include "idcollectionbindings.hpp" + +namespace +{ + struct FactionRank : ESM::RankData + { + std::string mRankName; + ESM::RefId mFactionId; + size_t mRankIndex; + + FactionRank(const ESM::RefId& factionId, const ESM::RankData& data, std::string_view rankName, size_t rankIndex) + : ESM::RankData(data) + , mRankName(rankName) + , mFactionId(factionId) + , mRankIndex(rankIndex) + { + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initCoreFactionBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table factions(lua, sol::create); + addRecordFunctionBinding(factions, context); + // Faction record + auto factionT = lua.new_usertype("ESM3_Faction"); + factionT[sol::meta_function::to_string] + = [](const ESM::Faction& rec) -> std::string { return "ESM3_Faction[" + rec.mId.toDebugString() + "]"; }; + factionT["id"] = sol::readonly_property([](const ESM::Faction& rec) { return rec.mId.serializeText(); }); + factionT["name"] + = sol::readonly_property([](const ESM::Faction& rec) -> std::string_view { return rec.mName; }); + factionT["hidden"] + = sol::readonly_property([](const ESM::Faction& rec) -> bool { return rec.mData.mIsHidden; }); + factionT["ranks"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + sol::table res(lua, sol::create); + for (size_t i = 0; i < rec.mRanks.size() && i < rec.mData.mRankData.size(); i++) + { + if (rec.mRanks[i].empty()) + break; + + res.add(FactionRank(rec.mId, rec.mData.mRankData[i], rec.mRanks[i], i)); + } + + return res; + }); + factionT["reactions"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + sol::table res(lua, sol::create); + for (const auto& [factionId, reaction] : rec.mReactions) + res[factionId.serializeText()] = reaction; + + const auto* overrides + = MWBase::Environment::get().getDialogueManager()->getFactionReactionOverrides(rec.mId); + + if (overrides != nullptr) + { + for (const auto& [factionId, reaction] : *overrides) + res[factionId.serializeText()] = reaction; + } + + return res; + }); + factionT["attributes"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + return createReadOnlyRefIdTable(lua, rec.mData.mAttribute, ESM::Attribute::indexToRefId); + }); + factionT["skills"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + return createReadOnlyRefIdTable(lua, rec.mData.mSkills, ESM::Skill::indexToRefId); + }); + auto rankT = lua.new_usertype("ESM3_FactionRank"); + rankT[sol::meta_function::to_string] = [](const FactionRank& rec) -> std::string { + return "ESM3_FactionRank[" + rec.mFactionId.toDebugString() + ", " + + std::to_string(LuaUtil::toLuaIndex(rec.mRankIndex)) + "]"; + }; + rankT["name"] + = sol::readonly_property([](const FactionRank& rec) -> std::string_view { return rec.mRankName; }); + rankT["primarySkillValue"] = sol::readonly_property([](const FactionRank& rec) { return rec.mPrimarySkill; }); + rankT["favouredSkillValue"] = sol::readonly_property([](const FactionRank& rec) { return rec.mFavouredSkill; }); + rankT["factionReaction"] = sol::readonly_property([](const FactionRank& rec) { return rec.mFactReaction; }); + rankT["attributeValues"] = sol::readonly_property([lua = lua.lua_state()](const FactionRank& rec) { + sol::table res(lua, sol::create); + res.add(rec.mAttribute1); + res.add(rec.mAttribute2); + return res; + }); + return LuaUtil::makeReadOnly(factions); + } +} diff --git a/apps/openmw/mwlua/factionbindings.hpp b/apps/openmw/mwlua/factionbindings.hpp new file mode 100644 index 0000000000..0dc06ceaf2 --- /dev/null +++ b/apps/openmw/mwlua/factionbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_FACTIONBINDINGS_H +#define MWLUA_FACTIONBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreFactionBindings(const Context& context); +} + +#endif // MWLUA_FACTIONBINDINGS_H diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index 0190000586..37fff22f99 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -1,10 +1,6 @@ #ifndef MWLUA_GLOBALSCRIPTS_H #define MWLUA_GLOBALSCRIPTS_H -#include -#include -#include - #include #include @@ -19,8 +15,16 @@ namespace MWLua GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") { - registerEngineHandlers({ &mObjectActiveHandlers, &mActorActiveHandlers, &mItemActiveHandlers, - &mNewGameHandlers, &mPlayerAddedHandlers, &mOnActivateHandlers, &mOnNewExteriorHandlers }); + registerEngineHandlers({ + &mObjectActiveHandlers, + &mActorActiveHandlers, + &mItemActiveHandlers, + &mNewGameHandlers, + &mPlayerAddedHandlers, + &mOnActivateHandlers, + &mOnUseItemHandlers, + &mOnNewExteriorHandlers, + }); } void newGameStarted() { callEngineHandlers(mNewGameHandlers); } @@ -32,6 +36,10 @@ namespace MWLua { callEngineHandlers(mOnActivateHandlers, obj, actor); } + void onUseItem(const GObject& obj, const GObject& actor, bool force) + { + callEngineHandlers(mOnUseItemHandlers, obj, actor, force); + } void onNewExterior(const GCell& cell) { callEngineHandlers(mOnNewExteriorHandlers, cell); } private: @@ -41,6 +49,7 @@ namespace MWLua EngineHandlerList mNewGameHandlers{ "onNewGame" }; EngineHandlerList mPlayerAddedHandlers{ "onPlayerAdded" }; EngineHandlerList mOnActivateHandlers{ "onActivate" }; + EngineHandlerList mOnUseItemHandlers{ "_onUseItem" }; EngineHandlerList mOnNewExteriorHandlers{ "onNewExterior" }; }; diff --git a/apps/openmw/mwlua/idcollectionbindings.hpp b/apps/openmw/mwlua/idcollectionbindings.hpp new file mode 100644 index 0000000000..329486aba1 --- /dev/null +++ b/apps/openmw/mwlua/idcollectionbindings.hpp @@ -0,0 +1,25 @@ +#ifndef MWLUA_IDCOLLECTIONBINDINGS_H +#define MWLUA_IDCOLLECTIONBINDINGS_H + +#include + +#include +#include + +namespace MWLua +{ + template + sol::table createReadOnlyRefIdTable(lua_State* lua, const C& container, P projection = {}) + { + sol::table res(lua, sol::create); + for (const auto& element : container) + { + ESM::RefId id = projection(element); + if (!id.empty()) + res.add(id.serializeText()); + } + return LuaUtil::makeReadOnly(res); + } +} + +#endif diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 9384eccdbc..b9affcdfca 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -4,19 +4,33 @@ #include #include +#include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwinput/actions.hpp" +#include "luamanagerimp.hpp" + namespace sol { template <> struct is_automagical : std::false_type { }; + + template <> + struct is_automagical : std::false_type + { + }; + + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua @@ -24,7 +38,13 @@ namespace MWLua sol::table initInputPackage(const Context& context) { - sol::usertype keyEvent = context.mLua->sol().new_usertype("KeyEvent"); + sol::state_view lua = context.sol(); + { + if (lua["openmw_input"] != sol::nil) + return lua["openmw_input"]; + } + + sol::usertype keyEvent = lua.new_usertype("KeyEvent"); keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { if (e.sym > 0 && e.sym <= 255) return std::string(1, static_cast(e.sym)); @@ -37,7 +57,7 @@ namespace MWLua keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); - auto touchpadEvent = context.mLua->sol().new_usertype("TouchpadEvent"); + auto touchpadEvent = lua.new_usertype("TouchpadEvent"); touchpadEvent["device"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mDevice; }); touchpadEvent["finger"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mFinger; }); touchpadEvent["position"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> osg::Vec2f { @@ -46,8 +66,132 @@ namespace MWLua touchpadEvent["pressure"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; }); + auto inputActions = lua.new_usertype("InputActions"); + inputActions[sol::meta_function::index] + = [](LuaUtil::InputAction::Registry& registry, std::string_view key) { return registry[key]; }; + { + auto pairs = [](LuaUtil::InputAction::Registry& registry) { + auto next + = [](LuaUtil::InputAction::Registry& registry, + std::string_view key) -> sol::optional> { + std::optional nextKey(registry.nextKey(key)); + if (!nextKey.has_value()) + return sol::nullopt; + else + return std::make_tuple(*nextKey, registry[*nextKey].value()); + }; + return std::make_tuple(next, registry, registry.firstKey()); + }; + inputActions[sol::meta_function::pairs] = pairs; + } + + auto actionInfo = lua.new_usertype("ActionInfo"); + actionInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mKey; }); + actionInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mName; }); + actionInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mDescription; }); + actionInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mL10n; }); + actionInfo["type"] = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mType; }); + actionInfo["defaultValue"] + = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; }); + + auto inputTriggers = lua.new_usertype("InputTriggers"); + inputTriggers[sol::meta_function::index] + = [](LuaUtil::InputTrigger::Registry& registry, std::string_view key) { return registry[key]; }; + { + auto pairs = [](LuaUtil::InputTrigger::Registry& registry) { + auto next + = [](LuaUtil::InputTrigger::Registry& registry, + std::string_view key) -> sol::optional> { + std::optional nextKey(registry.nextKey(key)); + if (!nextKey.has_value()) + return sol::nullopt; + else + return std::make_tuple(*nextKey, registry[*nextKey].value()); + }; + return std::make_tuple(next, registry, registry.firstKey()); + }; + inputTriggers[sol::meta_function::pairs] = pairs; + } + + auto triggerInfo = lua.new_usertype("TriggerInfo"); + triggerInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mKey; }); + triggerInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mName; }); + triggerInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mDescription; }); + triggerInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mL10n; }); + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); - sol::table api(context.mLua->sol(), sol::create); + sol::table api(lua, sol::create); + + api["ACTION_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Boolean", LuaUtil::InputAction::Type::Boolean }, + { "Number", LuaUtil::InputAction::Type::Number }, + { "Range", LuaUtil::InputAction::Type::Range }, + })); + + api["actions"] = std::ref(context.mLuaManager->inputActions()); + api["registerAction"] = [manager = context.mLuaManager](sol::table options) { + LuaUtil::InputAction::Info parsedOptions; + parsedOptions.mKey = options["key"].get(); + parsedOptions.mType = options["type"].get(); + parsedOptions.mL10n = options["l10n"].get(); + parsedOptions.mName = options["name"].get(); + parsedOptions.mDescription = options["description"].get(); + parsedOptions.mDefaultValue = options["defaultValue"].get(); + manager->inputActions().insert(std::move(parsedOptions)); + }; + api["bindAction"] = [manager = context.mLuaManager]( + std::string_view key, const sol::table& callback, sol::table dependencies) { + std::vector parsedDependencies; + parsedDependencies.reserve(dependencies.size()); + for (size_t i = 1; i <= dependencies.size(); ++i) + { + sol::object dependency = dependencies[i]; + if (!dependency.is()) + throw std::domain_error("The dependencies argument must be a list of Action keys"); + parsedDependencies.push_back(dependency.as()); + } + if (!manager->inputActions().bind(key, LuaUtil::Callback::fromLua(callback), parsedDependencies)) + throw std::domain_error("Cyclic action binding"); + }; + api["registerActionHandler"] + = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { + manager->inputActions().registerHandler(key, LuaUtil::Callback::fromLua(callback)); + }; + api["getBooleanActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Boolean); + }; + api["getNumberActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Number); + }; + api["getRangeActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Range); + }; + + api["triggers"] = std::ref(context.mLuaManager->inputTriggers()); + api["registerTrigger"] = [manager = context.mLuaManager](sol::table options) { + LuaUtil::InputTrigger::Info parsedOptions; + parsedOptions.mKey = options["key"].get(); + parsedOptions.mL10n = options["l10n"].get(); + parsedOptions.mName = options["name"].get(); + parsedOptions.mDescription = options["description"].get(); + manager->inputTriggers().insert(std::move(parsedOptions)); + }; + api["registerTriggerHandler"] + = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { + manager->inputTriggers().registerHandler(key, LuaUtil::Callback::fromLua(callback)); + }; + api["activateTrigger"] + = [manager = context.mLuaManager](std::string_view key) { manager->inputTriggers().activate(key); }; api["isIdle"] = [input]() { return input->isIdle(); }; api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); }; @@ -68,6 +212,11 @@ namespace MWLua }; api["isMouseButtonPressed"] = [](int button) -> bool { return SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button); }; + api["_isGamepadCursorActive"] = [input]() -> bool { return input->isGamepadGuiCursorEnabled(); }; + api["_setGamepadCursorActive"] = [input](bool v) { + input->setGamepadGuiCursorEnabled(v); + MWBase::Environment::get().getWindowManager()->setCursorActive(v); + }; api["getMouseMoveX"] = [input]() { return input->getMouseMoveX(); }; api["getMouseMoveY"] = [input]() { return input->getMouseMoveY(); }; api["getAxisValue"] = [input](int axis) { @@ -77,219 +226,227 @@ namespace MWLua return input->getActionValue(axis - SDL_CONTROLLER_AXIS_MAX) * 2 - 1; }; + // input.CONTROL_SWITCH is deprecated, remove after releasing 0.49 api["getControlSwitch"] = [input](std::string_view key) { return input->getControlSwitch(key); }; api["setControlSwitch"] = [input](std::string_view key, bool v) { input->toggleControlSwitch(key, v); }; - api["getKeyName"] = [](SDL_Scancode code) { return SDL_GetKeyName(SDL_GetKeyFromScancode(code)); }; + api["getKeyName"] = [](SDL_Scancode code) { return SDL_GetScancodeName(code); }; - api["ACTION"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "GameMenu", MWInput::A_GameMenu }, - { "Screenshot", MWInput::A_Screenshot }, - { "Inventory", MWInput::A_Inventory }, - { "Console", MWInput::A_Console }, + api["ACTION"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "GameMenu", MWInput::A_GameMenu }, + { "Screenshot", MWInput::A_Screenshot }, + { "Inventory", MWInput::A_Inventory }, + { "Console", MWInput::A_Console }, - { "MoveLeft", MWInput::A_MoveLeft }, - { "MoveRight", MWInput::A_MoveRight }, - { "MoveForward", MWInput::A_MoveForward }, - { "MoveBackward", MWInput::A_MoveBackward }, + { "MoveLeft", MWInput::A_MoveLeft }, + { "MoveRight", MWInput::A_MoveRight }, + { "MoveForward", MWInput::A_MoveForward }, + { "MoveBackward", MWInput::A_MoveBackward }, - { "Activate", MWInput::A_Activate }, - { "Use", MWInput::A_Use }, - { "Jump", MWInput::A_Jump }, - { "AutoMove", MWInput::A_AutoMove }, - { "Rest", MWInput::A_Rest }, - { "Journal", MWInput::A_Journal }, - { "Run", MWInput::A_Run }, - { "CycleSpellLeft", MWInput::A_CycleSpellLeft }, - { "CycleSpellRight", MWInput::A_CycleSpellRight }, - { "CycleWeaponLeft", MWInput::A_CycleWeaponLeft }, - { "CycleWeaponRight", MWInput::A_CycleWeaponRight }, - { "AlwaysRun", MWInput::A_AlwaysRun }, - { "Sneak", MWInput::A_Sneak }, + { "Activate", MWInput::A_Activate }, + { "Use", MWInput::A_Use }, + { "Jump", MWInput::A_Jump }, + { "AutoMove", MWInput::A_AutoMove }, + { "Rest", MWInput::A_Rest }, + { "Journal", MWInput::A_Journal }, + { "Run", MWInput::A_Run }, + { "CycleSpellLeft", MWInput::A_CycleSpellLeft }, + { "CycleSpellRight", MWInput::A_CycleSpellRight }, + { "CycleWeaponLeft", MWInput::A_CycleWeaponLeft }, + { "CycleWeaponRight", MWInput::A_CycleWeaponRight }, + { "AlwaysRun", MWInput::A_AlwaysRun }, + { "Sneak", MWInput::A_Sneak }, - { "QuickSave", MWInput::A_QuickSave }, - { "QuickLoad", MWInput::A_QuickLoad }, - { "QuickMenu", MWInput::A_QuickMenu }, - { "ToggleWeapon", MWInput::A_ToggleWeapon }, - { "ToggleSpell", MWInput::A_ToggleSpell }, - { "TogglePOV", MWInput::A_TogglePOV }, + { "QuickSave", MWInput::A_QuickSave }, + { "QuickLoad", MWInput::A_QuickLoad }, + { "QuickMenu", MWInput::A_QuickMenu }, + { "ToggleWeapon", MWInput::A_ToggleWeapon }, + { "ToggleSpell", MWInput::A_ToggleSpell }, + { "TogglePOV", MWInput::A_TogglePOV }, - { "QuickKey1", MWInput::A_QuickKey1 }, - { "QuickKey2", MWInput::A_QuickKey2 }, - { "QuickKey3", MWInput::A_QuickKey3 }, - { "QuickKey4", MWInput::A_QuickKey4 }, - { "QuickKey5", MWInput::A_QuickKey5 }, - { "QuickKey6", MWInput::A_QuickKey6 }, - { "QuickKey7", MWInput::A_QuickKey7 }, - { "QuickKey8", MWInput::A_QuickKey8 }, - { "QuickKey9", MWInput::A_QuickKey9 }, - { "QuickKey10", MWInput::A_QuickKey10 }, - { "QuickKeysMenu", MWInput::A_QuickKeysMenu }, + { "QuickKey1", MWInput::A_QuickKey1 }, + { "QuickKey2", MWInput::A_QuickKey2 }, + { "QuickKey3", MWInput::A_QuickKey3 }, + { "QuickKey4", MWInput::A_QuickKey4 }, + { "QuickKey5", MWInput::A_QuickKey5 }, + { "QuickKey6", MWInput::A_QuickKey6 }, + { "QuickKey7", MWInput::A_QuickKey7 }, + { "QuickKey8", MWInput::A_QuickKey8 }, + { "QuickKey9", MWInput::A_QuickKey9 }, + { "QuickKey10", MWInput::A_QuickKey10 }, + { "QuickKeysMenu", MWInput::A_QuickKeysMenu }, - { "ToggleHUD", MWInput::A_ToggleHUD }, - { "ToggleDebug", MWInput::A_ToggleDebug }, - { "TogglePostProcessorHUD", MWInput::A_TogglePostProcessorHUD }, + { "ToggleHUD", MWInput::A_ToggleHUD }, + { "ToggleDebug", MWInput::A_ToggleDebug }, + { "TogglePostProcessorHUD", MWInput::A_TogglePostProcessorHUD }, - { "ZoomIn", MWInput::A_ZoomIn }, - { "ZoomOut", MWInput::A_ZoomOut }, - })); - - api["CONTROL_SWITCH"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Controls", "playercontrols" }, - { "Fighting", "playerfighting" }, - { "Jumping", "playerjumping" }, - { "Looking", "playerlooking" }, - { "Magic", "playermagic" }, - { "ViewMode", "playerviewswitch" }, - { "VanityMode", "vanitymode" }, + { "ZoomIn", MWInput::A_ZoomIn }, + { "ZoomOut", MWInput::A_ZoomOut }, })); + // input.CONTROL_SWITCH is deprecated, remove after releasing 0.49 + api["CONTROL_SWITCH"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Controls", "playercontrols" }, + { "Fighting", "playerfighting" }, + { "Jumping", "playerjumping" }, + { "Looking", "playerlooking" }, + { "Magic", "playermagic" }, + { "ViewMode", "playerviewswitch" }, + { "VanityMode", "vanitymode" }, + })); + api["CONTROLLER_BUTTON"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "A", SDL_CONTROLLER_BUTTON_A }, - { "B", SDL_CONTROLLER_BUTTON_B }, - { "X", SDL_CONTROLLER_BUTTON_X }, - { "Y", SDL_CONTROLLER_BUTTON_Y }, - { "Back", SDL_CONTROLLER_BUTTON_BACK }, - { "Guide", SDL_CONTROLLER_BUTTON_GUIDE }, - { "Start", SDL_CONTROLLER_BUTTON_START }, - { "LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK }, - { "RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK }, - { "LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER }, - { "RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER }, - { "DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP }, - { "DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN }, - { "DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT }, - { "DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT }, + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "A", SDL_CONTROLLER_BUTTON_A }, + { "B", SDL_CONTROLLER_BUTTON_B }, + { "X", SDL_CONTROLLER_BUTTON_X }, + { "Y", SDL_CONTROLLER_BUTTON_Y }, + { "Back", SDL_CONTROLLER_BUTTON_BACK }, + { "Guide", SDL_CONTROLLER_BUTTON_GUIDE }, + { "Start", SDL_CONTROLLER_BUTTON_START }, + { "LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK }, + { "RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK }, + { "LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER }, + { "RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER }, + { "DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP }, + { "DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN }, + { "DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT }, + { "DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT }, + })); + + api["CONTROLLER_AXIS"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "LeftX", SDL_CONTROLLER_AXIS_LEFTX }, + { "LeftY", SDL_CONTROLLER_AXIS_LEFTY }, + { "RightX", SDL_CONTROLLER_AXIS_RIGHTX }, + { "RightY", SDL_CONTROLLER_AXIS_RIGHTY }, + { "TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT }, + { "TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT }, + + { "LookUpDown", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_LookUpDown) }, + { "LookLeftRight", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_LookLeftRight) }, + { "MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_MoveForwardBackward) }, + { "MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_MoveLeftRight) }, })); - api["CONTROLLER_AXIS"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "LeftX", SDL_CONTROLLER_AXIS_LEFTX }, - { "LeftY", SDL_CONTROLLER_AXIS_LEFTY }, - { "RightX", SDL_CONTROLLER_AXIS_RIGHTX }, - { "RightY", SDL_CONTROLLER_AXIS_RIGHTY }, - { "TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT }, - { "TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT }, + api["KEY"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "_0", SDL_SCANCODE_0 }, + { "_1", SDL_SCANCODE_1 }, + { "_2", SDL_SCANCODE_2 }, + { "_3", SDL_SCANCODE_3 }, + { "_4", SDL_SCANCODE_4 }, + { "_5", SDL_SCANCODE_5 }, + { "_6", SDL_SCANCODE_6 }, + { "_7", SDL_SCANCODE_7 }, + { "_8", SDL_SCANCODE_8 }, + { "_9", SDL_SCANCODE_9 }, - { "LookUpDown", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_LookUpDown) }, - { "LookLeftRight", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_LookLeftRight) }, - { "MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_MoveForwardBackward) }, - { "MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_MoveLeftRight) }, - })); + { "NP_0", SDL_SCANCODE_KP_0 }, + { "NP_1", SDL_SCANCODE_KP_1 }, + { "NP_2", SDL_SCANCODE_KP_2 }, + { "NP_3", SDL_SCANCODE_KP_3 }, + { "NP_4", SDL_SCANCODE_KP_4 }, + { "NP_5", SDL_SCANCODE_KP_5 }, + { "NP_6", SDL_SCANCODE_KP_6 }, + { "NP_7", SDL_SCANCODE_KP_7 }, + { "NP_8", SDL_SCANCODE_KP_8 }, + { "NP_9", SDL_SCANCODE_KP_9 }, + { "NP_Divide", SDL_SCANCODE_KP_DIVIDE }, + { "NP_Enter", SDL_SCANCODE_KP_ENTER }, + { "NP_Minus", SDL_SCANCODE_KP_MINUS }, + { "NP_Multiply", SDL_SCANCODE_KP_MULTIPLY }, + { "NP_Delete", SDL_SCANCODE_KP_PERIOD }, + { "NP_Plus", SDL_SCANCODE_KP_PLUS }, - api["KEY"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "_0", SDL_SCANCODE_0 }, - { "_1", SDL_SCANCODE_1 }, - { "_2", SDL_SCANCODE_2 }, - { "_3", SDL_SCANCODE_3 }, - { "_4", SDL_SCANCODE_4 }, - { "_5", SDL_SCANCODE_5 }, - { "_6", SDL_SCANCODE_6 }, - { "_7", SDL_SCANCODE_7 }, - { "_8", SDL_SCANCODE_8 }, - { "_9", SDL_SCANCODE_9 }, + { "F1", SDL_SCANCODE_F1 }, + { "F2", SDL_SCANCODE_F2 }, + { "F3", SDL_SCANCODE_F3 }, + { "F4", SDL_SCANCODE_F4 }, + { "F5", SDL_SCANCODE_F5 }, + { "F6", SDL_SCANCODE_F6 }, + { "F7", SDL_SCANCODE_F7 }, + { "F8", SDL_SCANCODE_F8 }, + { "F9", SDL_SCANCODE_F9 }, + { "F10", SDL_SCANCODE_F10 }, + { "F11", SDL_SCANCODE_F11 }, + { "F12", SDL_SCANCODE_F12 }, - { "NP_0", SDL_SCANCODE_KP_0 }, - { "NP_1", SDL_SCANCODE_KP_1 }, - { "NP_2", SDL_SCANCODE_KP_2 }, - { "NP_3", SDL_SCANCODE_KP_3 }, - { "NP_4", SDL_SCANCODE_KP_4 }, - { "NP_5", SDL_SCANCODE_KP_5 }, - { "NP_6", SDL_SCANCODE_KP_6 }, - { "NP_7", SDL_SCANCODE_KP_7 }, - { "NP_8", SDL_SCANCODE_KP_8 }, - { "NP_9", SDL_SCANCODE_KP_9 }, - { "NP_Divide", SDL_SCANCODE_KP_DIVIDE }, - { "NP_Enter", SDL_SCANCODE_KP_ENTER }, - { "NP_Minus", SDL_SCANCODE_KP_MINUS }, - { "NP_Multiply", SDL_SCANCODE_KP_MULTIPLY }, - { "NP_Delete", SDL_SCANCODE_KP_PERIOD }, - { "NP_Plus", SDL_SCANCODE_KP_PLUS }, + { "A", SDL_SCANCODE_A }, + { "B", SDL_SCANCODE_B }, + { "C", SDL_SCANCODE_C }, + { "D", SDL_SCANCODE_D }, + { "E", SDL_SCANCODE_E }, + { "F", SDL_SCANCODE_F }, + { "G", SDL_SCANCODE_G }, + { "H", SDL_SCANCODE_H }, + { "I", SDL_SCANCODE_I }, + { "J", SDL_SCANCODE_J }, + { "K", SDL_SCANCODE_K }, + { "L", SDL_SCANCODE_L }, + { "M", SDL_SCANCODE_M }, + { "N", SDL_SCANCODE_N }, + { "O", SDL_SCANCODE_O }, + { "P", SDL_SCANCODE_P }, + { "Q", SDL_SCANCODE_Q }, + { "R", SDL_SCANCODE_R }, + { "S", SDL_SCANCODE_S }, + { "T", SDL_SCANCODE_T }, + { "U", SDL_SCANCODE_U }, + { "V", SDL_SCANCODE_V }, + { "W", SDL_SCANCODE_W }, + { "X", SDL_SCANCODE_X }, + { "Y", SDL_SCANCODE_Y }, + { "Z", SDL_SCANCODE_Z }, - { "F1", SDL_SCANCODE_F1 }, - { "F2", SDL_SCANCODE_F2 }, - { "F3", SDL_SCANCODE_F3 }, - { "F4", SDL_SCANCODE_F4 }, - { "F5", SDL_SCANCODE_F5 }, - { "F6", SDL_SCANCODE_F6 }, - { "F7", SDL_SCANCODE_F7 }, - { "F8", SDL_SCANCODE_F8 }, - { "F9", SDL_SCANCODE_F9 }, - { "F10", SDL_SCANCODE_F10 }, - { "F11", SDL_SCANCODE_F11 }, - { "F12", SDL_SCANCODE_F12 }, + { "LeftArrow", SDL_SCANCODE_LEFT }, + { "RightArrow", SDL_SCANCODE_RIGHT }, + { "UpArrow", SDL_SCANCODE_UP }, + { "DownArrow", SDL_SCANCODE_DOWN }, - { "A", SDL_SCANCODE_A }, - { "B", SDL_SCANCODE_B }, - { "C", SDL_SCANCODE_C }, - { "D", SDL_SCANCODE_D }, - { "E", SDL_SCANCODE_E }, - { "F", SDL_SCANCODE_F }, - { "G", SDL_SCANCODE_G }, - { "H", SDL_SCANCODE_H }, - { "I", SDL_SCANCODE_I }, - { "J", SDL_SCANCODE_J }, - { "K", SDL_SCANCODE_K }, - { "L", SDL_SCANCODE_L }, - { "M", SDL_SCANCODE_M }, - { "N", SDL_SCANCODE_N }, - { "O", SDL_SCANCODE_O }, - { "P", SDL_SCANCODE_P }, - { "Q", SDL_SCANCODE_Q }, - { "R", SDL_SCANCODE_R }, - { "S", SDL_SCANCODE_S }, - { "T", SDL_SCANCODE_T }, - { "U", SDL_SCANCODE_U }, - { "V", SDL_SCANCODE_V }, - { "W", SDL_SCANCODE_W }, - { "X", SDL_SCANCODE_X }, - { "Y", SDL_SCANCODE_Y }, - { "Z", SDL_SCANCODE_Z }, + { "LeftAlt", SDL_SCANCODE_LALT }, + { "LeftCtrl", SDL_SCANCODE_LCTRL }, + { "LeftBracket", SDL_SCANCODE_LEFTBRACKET }, + { "LeftSuper", SDL_SCANCODE_LGUI }, + { "LeftShift", SDL_SCANCODE_LSHIFT }, + { "RightAlt", SDL_SCANCODE_RALT }, + { "RightCtrl", SDL_SCANCODE_RCTRL }, + { "RightSuper", SDL_SCANCODE_RGUI }, + { "RightBracket", SDL_SCANCODE_RIGHTBRACKET }, + { "RightShift", SDL_SCANCODE_RSHIFT }, - { "LeftArrow", SDL_SCANCODE_LEFT }, - { "RightArrow", SDL_SCANCODE_RIGHT }, - { "UpArrow", SDL_SCANCODE_UP }, - { "DownArrow", SDL_SCANCODE_DOWN }, + { "Apostrophe", SDL_SCANCODE_APOSTROPHE }, + { "BackSlash", SDL_SCANCODE_BACKSLASH }, + { "Backspace", SDL_SCANCODE_BACKSPACE }, + { "CapsLock", SDL_SCANCODE_CAPSLOCK }, + { "Comma", SDL_SCANCODE_COMMA }, + { "Delete", SDL_SCANCODE_DELETE }, + { "End", SDL_SCANCODE_END }, + { "Enter", SDL_SCANCODE_RETURN }, + { "Equals", SDL_SCANCODE_EQUALS }, + { "Escape", SDL_SCANCODE_ESCAPE }, + { "Home", SDL_SCANCODE_HOME }, + { "Insert", SDL_SCANCODE_INSERT }, + { "Minus", SDL_SCANCODE_MINUS }, + { "NumLock", SDL_SCANCODE_NUMLOCKCLEAR }, + { "PageDown", SDL_SCANCODE_PAGEDOWN }, + { "PageUp", SDL_SCANCODE_PAGEUP }, + { "Period", SDL_SCANCODE_PERIOD }, + { "Pause", SDL_SCANCODE_PAUSE }, + { "PrintScreen", SDL_SCANCODE_PRINTSCREEN }, + { "ScrollLock", SDL_SCANCODE_SCROLLLOCK }, + { "Semicolon", SDL_SCANCODE_SEMICOLON }, + { "Slash", SDL_SCANCODE_SLASH }, + { "Space", SDL_SCANCODE_SPACE }, + { "Tab", SDL_SCANCODE_TAB }, + })); - { "LeftAlt", SDL_SCANCODE_LALT }, - { "LeftCtrl", SDL_SCANCODE_LCTRL }, - { "LeftBracket", SDL_SCANCODE_LEFTBRACKET }, - { "LeftSuper", SDL_SCANCODE_LGUI }, - { "LeftShift", SDL_SCANCODE_LSHIFT }, - { "RightAlt", SDL_SCANCODE_RALT }, - { "RightCtrl", SDL_SCANCODE_RCTRL }, - { "RightSuper", SDL_SCANCODE_RGUI }, - { "RightBracket", SDL_SCANCODE_RIGHTBRACKET }, - { "RightShift", SDL_SCANCODE_RSHIFT }, - - { "Apostrophe", SDL_SCANCODE_APOSTROPHE }, - { "BackSlash", SDL_SCANCODE_BACKSLASH }, - { "Backspace", SDL_SCANCODE_BACKSPACE }, - { "CapsLock", SDL_SCANCODE_CAPSLOCK }, - { "Comma", SDL_SCANCODE_COMMA }, - { "Delete", SDL_SCANCODE_DELETE }, - { "End", SDL_SCANCODE_END }, - { "Enter", SDL_SCANCODE_RETURN }, - { "Equals", SDL_SCANCODE_EQUALS }, - { "Escape", SDL_SCANCODE_ESCAPE }, - { "Home", SDL_SCANCODE_HOME }, - { "Insert", SDL_SCANCODE_INSERT }, - { "Minus", SDL_SCANCODE_MINUS }, - { "NumLock", SDL_SCANCODE_NUMLOCKCLEAR }, - { "PageDown", SDL_SCANCODE_PAGEDOWN }, - { "PageUp", SDL_SCANCODE_PAGEUP }, - { "Period", SDL_SCANCODE_PERIOD }, - { "Pause", SDL_SCANCODE_PAUSE }, - { "PrintScreen", SDL_SCANCODE_PRINTSCREEN }, - { "ScrollLock", SDL_SCANCODE_SCROLLLOCK }, - { "Semicolon", SDL_SCANCODE_SEMICOLON }, - { "Slash", SDL_SCANCODE_SLASH }, - { "Space", SDL_SCANCODE_SPACE }, - { "Tab", SDL_SCANCODE_TAB }, - })); - - return LuaUtil::makeReadOnly(api); + lua["openmw_input"] = LuaUtil::makeReadOnly(api); + return lua["openmw_input"]; } } diff --git a/apps/openmw/mwlua/inputprocessor.hpp b/apps/openmw/mwlua/inputprocessor.hpp new file mode 100644 index 0000000000..dcd19ae8cd --- /dev/null +++ b/apps/openmw/mwlua/inputprocessor.hpp @@ -0,0 +1,85 @@ +#ifndef MWLUA_INPUTPROCESSOR_H +#define MWLUA_INPUTPROCESSOR_H + +#include + +#include + +#include "../mwbase/luamanager.hpp" + +namespace MWLua +{ + template + class InputProcessor + { + public: + InputProcessor(Container* scriptsContainer) + : mScriptsContainer(scriptsContainer) + { + mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers, + &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mTouchpadPressed, + &mTouchpadReleased, &mTouchpadMoved, &mMouseButtonPress, &mMouseButtonRelease, &mMouseWheel }); + } + + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + using InputEvent = MWBase::LuaManager::InputEvent; + switch (event.mType) + { + case InputEvent::KeyPressed: + mScriptsContainer->callEngineHandlers(mKeyPressHandlers, std::get(event.mValue)); + break; + case InputEvent::KeyReleased: + mScriptsContainer->callEngineHandlers(mKeyReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerPressed: + mScriptsContainer->callEngineHandlers(mControllerButtonPressHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerReleased: + mScriptsContainer->callEngineHandlers( + mControllerButtonReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::Action: + mScriptsContainer->callEngineHandlers(mActionHandlers, std::get(event.mValue)); + break; + case InputEvent::TouchPressed: + mScriptsContainer->callEngineHandlers( + mTouchpadPressed, std::get(event.mValue)); + break; + case InputEvent::TouchReleased: + mScriptsContainer->callEngineHandlers( + mTouchpadReleased, std::get(event.mValue)); + break; + case InputEvent::TouchMoved: + mScriptsContainer->callEngineHandlers(mTouchpadMoved, std::get(event.mValue)); + break; + case InputEvent::MouseButtonPressed: + mScriptsContainer->callEngineHandlers(mMouseButtonPress, std::get(event.mValue)); + break; + case InputEvent::MouseButtonReleased: + mScriptsContainer->callEngineHandlers(mMouseButtonRelease, std::get(event.mValue)); + break; + case InputEvent::MouseWheel: + auto wheelEvent = std::get(event.mValue); + mScriptsContainer->callEngineHandlers(mMouseWheel, wheelEvent.y, wheelEvent.x); + break; + } + } + + private: + Container* mScriptsContainer; + typename Container::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; + typename Container::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; + typename Container::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; + typename Container::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; + typename Container::EngineHandlerList mActionHandlers{ "onInputAction" }; + typename Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; + typename Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; + typename Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; + typename Container::EngineHandlerList mMouseButtonPress{ "onMouseButtonPress" }; + typename Container::EngineHandlerList mMouseButtonRelease{ "onMouseButtonRelease" }; + typename Container::EngineHandlerList mMouseWheel{ "onMouseWheel" }; + }; +} + +#endif // MWLUA_INPUTPROCESSOR_H diff --git a/apps/openmw/mwlua/itemdata.cpp b/apps/openmw/mwlua/itemdata.cpp new file mode 100644 index 0000000000..38415ea25e --- /dev/null +++ b/apps/openmw/mwlua/itemdata.cpp @@ -0,0 +1,200 @@ +#include "itemdata.hpp" + +#include +#include + +#include "context.hpp" +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwmechanics/spellutil.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +namespace +{ + using SelfObject = MWLua::SelfObject; + using Index = const SelfObject::CachedStat::Index&; + + constexpr std::array properties = { "condition", "enchantmentCharge", "soul" }; + + void valueErr(std::string_view prop, std::string type) + { + throw std::logic_error("'" + std::string(prop) + "'" + " received invalid value type (" + type + ")"); + } +} + +namespace MWLua +{ + static void addStatUpdateAction(MWLua::LuaManager* manager, const SelfObject& obj) + { + if (!obj.mStatsCache.empty()) + return; // was already added before + manager->addAction( + [obj = Object(obj)] { + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->applyStatsCache(); + }, + "StatUpdateAction"); + } + + class ItemData + { + ObjectVariant mObject; + + public: + ItemData(const ObjectVariant& object) + : mObject(object) + { + } + + sol::object get(const Context& context, std::string_view prop) const + { + if (mObject.isSelfObject()) + { + SelfObject* self = mObject.asSelfObject(); + auto it = self->mStatsCache.find({ &ItemData::setValue, std::monostate{}, prop }); + if (it != self->mStatsCache.end()) + return it->second; + } + return sol::make_object(context.mLua->unsafeState(), getValue(context, prop, mObject.ptr())); + } + + void set(const Context& context, std::string_view prop, const sol::object& value) const + { + if (mObject.isGObject()) + setValue({}, prop, mObject.ptr(), value); + else if (mObject.isSelfObject()) + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] = value; + } + else + throw std::runtime_error("Only global or self scripts can set the value"); + } + + static sol::object getValue(const Context& context, std::string_view prop, const MWWorld::Ptr& ptr) + { + if (prop == "condition") + { + if (ptr.mRef->getType() == ESM::REC_LIGH) + return sol::make_object(context.mLua->unsafeState(), ptr.getClass().getRemainingUsageTime(ptr)); + else if (ptr.getClass().hasItemHealth(ptr)) + return sol::make_object(context.mLua->unsafeState(), + ptr.getClass().getItemHealth(ptr) + ptr.getCellRef().getChargeIntRemainder()); + } + else if (prop == "enchantmentCharge") + { + const ESM::RefId& enchantmentName = ptr.getClass().getEnchantment(ptr); + + if (enchantmentName.empty()) + return sol::lua_nil; + + float charge = ptr.getCellRef().getEnchantmentCharge(); + const auto& store = MWBase::Environment::get().getESMStore(); + const auto* enchantment = store->get().find(enchantmentName); + + if (charge == -1) // return the full charge + return sol::make_object( + context.mLua->unsafeState(), MWMechanics::getEnchantmentCharge(*enchantment)); + + return sol::make_object(context.mLua->unsafeState(), charge); + } + else if (prop == "soul") + { + ESM::RefId soul = ptr.getCellRef().getSoul(); + if (soul.empty()) + return sol::lua_nil; + + return sol::make_object(context.mLua->unsafeState(), soul.serializeText()); + } + + return sol::lua_nil; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + if (prop == "condition") + { + if (value.get_type() == sol::type::number) + { + float cond = LuaUtil::cast(value); + if (ptr.mRef->getType() == ESM::REC_LIGH) + ptr.getClass().setRemainingUsageTime(ptr, cond); + else if (ptr.getClass().hasItemHealth(ptr)) + { + // if the value set is less than 0, chargeInt and chargeIntRemainder is set to 0 + ptr.getCellRef().setChargeIntRemainder(std::max(0.f, std::modf(cond, &cond))); + ptr.getCellRef().setCharge(std::max(0.f, cond)); + } + } + else + valueErr(prop, sol::type_name(value.lua_state(), value.get_type())); + } + else if (prop == "enchantmentCharge") + { + if (value.get_type() == sol::type::lua_nil) + ptr.getCellRef().setEnchantmentCharge(-1); + else if (value.get_type() == sol::type::number) + ptr.getCellRef().setEnchantmentCharge(std::max(0.0f, LuaUtil::cast(value))); + else + valueErr(prop, sol::type_name(value.lua_state(), value.get_type())); + } + else if (prop == "soul") + { + if (value.get_type() == sol::type::lua_nil) + ptr.getCellRef().setSoul(ESM::RefId{}); + else if (value.get_type() == sol::type::string) + { + std::string_view souldId = LuaUtil::cast(value); + ESM::RefId creature = ESM::RefId::deserializeText(souldId); + const auto& store = *MWBase::Environment::get().getESMStore(); + + // TODO: Add Support for NPC Souls + if (store.get().search(creature)) + ptr.getCellRef().setSoul(creature); + else + throw std::runtime_error("Cannot use non-existent creature as a soul: " + std::string(souldId)); + } + else + valueErr(prop, sol::type_name(value.lua_state(), value.get_type())); + } + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addItemDataBindings(sol::table& item, const Context& context) + { + item["itemData"] = [](const sol::object& object) -> sol::optional { + ObjectVariant o(object); + if (o.ptr().getClass().isItem(o.ptr()) || o.ptr().mRef->getType() == ESM::REC_LIGH) + return ItemData(o); + return {}; + }; + + sol::usertype itemData = context.sol().new_usertype("ItemData"); + itemData[sol::meta_function::new_index] = [](const ItemData& stat, const sol::variadic_args args) { + throw std::runtime_error("Unknown ItemData property '" + args.get() + "'"); + }; + + for (std::string_view prop : properties) + { + itemData[prop] = sol::property([context, prop](const ItemData& stat) { return stat.get(context, prop); }, + [context, prop](const ItemData& stat, const sol::object& value) { stat.set(context, prop, value); }); + } + } +} diff --git a/apps/openmw/mwlua/itemdata.hpp b/apps/openmw/mwlua/itemdata.hpp new file mode 100644 index 0000000000..f70705fb6c --- /dev/null +++ b/apps/openmw/mwlua/itemdata.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_ITEMDATA_H +#define MWLUA_ITEMDATA_H + +#include + +namespace MWLua +{ + struct Context; + + void addItemDataBindings(sol::table& item, const Context& context); + +} +#endif // MWLUA_ITEMDATA_H diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8cf383e985..7a3e9ff23a 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -1,8 +1,11 @@ #include "localscripts.hpp" #include +#include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/aicombat.hpp" #include "../mwmechanics/aiescort.hpp" #include "../mwmechanics/aifollow.hpp" @@ -11,6 +14,7 @@ #include "../mwmechanics/aisequence.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aiwander.hpp" +#include "../mwmechanics/attacktype.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" @@ -34,8 +38,9 @@ namespace MWLua void LocalScripts::initializeSelfPackage(const Context& context) { + auto lua = context.sol(); using ActorControls = MWBase::LuaManager::ActorControls; - sol::usertype controls = context.mLua->sol().new_usertype("ActorControls"); + sol::usertype controls = lua.new_usertype("ActorControls"); #define CONTROL(TYPE, FIELD) \ sol::property([](const ActorControls& c) { return c.FIELD; }, \ @@ -53,17 +58,22 @@ namespace MWLua controls["use"] = CONTROL(int, mUse); #undef CONTROL - sol::usertype selfAPI = context.mLua->sol().new_usertype( - "SelfObject", sol::base_classes, sol::bases()); + sol::usertype selfAPI + = lua.new_usertype("SelfObject", sol::base_classes, sol::bases()); selfAPI[sol::meta_function::to_string] = [](SelfObject& self) { return "openmw.self[" + self.toString() + "]"; }; selfAPI["object"] = sol::readonly_property([](SelfObject& self) -> LObject { return LObject(self); }); selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; }); selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; }; selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; }; + selfAPI["ATTACK_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { { "NoAttack", MWMechanics::AttackType::NoAttack }, { "Any", MWMechanics::AttackType::Any }, + { "Chop", MWMechanics::AttackType::Chop }, { "Slash", MWMechanics::AttackType::Slash }, + { "Thrust", MWMechanics::AttackType::Thrust } })); using AiPackage = MWMechanics::AiPackage; - sol::usertype aiPackage = context.mLua->sol().new_usertype("AiPackage"); + sol::usertype aiPackage = lua.new_usertype("AiPackage"); aiPackage["type"] = sol::readonly_property([](const AiPackage& p) -> std::string_view { switch (p.getTypeId()) { @@ -102,7 +112,37 @@ namespace MWLua }); aiPackage["sideWithTarget"] = sol::readonly_property([](const AiPackage& p) { return p.sideWithTarget(); }); aiPackage["destPosition"] = sol::readonly_property([](const AiPackage& p) { return p.getDestination(); }); + aiPackage["distance"] = sol::readonly_property([](const AiPackage& p) { return p.getDistance(); }); + aiPackage["duration"] = sol::readonly_property([](const AiPackage& p) { return p.getDuration(); }); + aiPackage["idle"] + = sol::readonly_property([lua = lua.lua_state()](const AiPackage& p) -> sol::optional { + if (p.getTypeId() == MWMechanics::AiPackageTypeId::Wander) + { + sol::table idles(lua, sol::create); + const std::vector& idle = static_cast(p).getIdle(); + if (!idle.empty()) + { + for (size_t i = 0; i < idle.size(); ++i) + { + std::string_view groupName = MWMechanics::AiWander::getIdleGroupName(i); + idles[groupName] = idle[i]; + } + return idles; + } + } + return sol::nullopt; + }); + aiPackage["isRepeat"] = sol::readonly_property([](const AiPackage& p) { return p.getRepeat(); }); + + selfAPI["_isFleeing"] = [](SelfObject& self) -> bool { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + if (ai.isEmpty()) + return false; + else + return ai.isFleeing(); + }; selfAPI["_getActiveAiPackage"] = [](SelfObject& self) -> sol::optional> { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); @@ -130,13 +170,25 @@ namespace MWLua MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); ai.stack(MWMechanics::AiPursue(target.ptr()), ptr, cancelOther); }; - selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target, bool cancelOther) { + selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target, sol::optional cell, + float duration, const osg::Vec3f& dest, bool repeat, bool cancelOther) { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - ai.stack(MWMechanics::AiFollow(target.ptr()), ptr, cancelOther); + if (cell) + { + ai.stack(MWMechanics::AiFollow(target.ptr().getCellRef().getRefId(), + cell->mStore->getCell()->getNameId(), duration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + } + else + { + ai.stack(MWMechanics::AiFollow( + target.ptr().getCellRef().getRefId(), duration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + } }; selfAPI["_startAiEscort"] = [](SelfObject& self, const LObject& target, LCell cell, float duration, - const osg::Vec3f& dest, bool cancelOther) { + const osg::Vec3f& dest, bool repeat, bool cancelOther) { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); // TODO: change AiEscort implementation to accept ptr instead of a non-unique refId. @@ -144,33 +196,43 @@ namespace MWLua int gameHoursDuration = static_cast(std::ceil(duration / 3600.0)); auto* esmCell = cell.mStore->getCell(); if (esmCell->isExterior()) - ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), false), ptr, + ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), repeat), ptr, cancelOther); else ai.stack(MWMechanics::AiEscort( - refId, esmCell->getNameId(), gameHoursDuration, dest.x(), dest.y(), dest.z(), false), + refId, esmCell->getNameId(), gameHoursDuration, dest.x(), dest.y(), dest.z(), repeat), ptr, cancelOther); }; - selfAPI["_startAiWander"] = [](SelfObject& self, int distance, float duration, bool cancelOther) { + selfAPI["_startAiWander"] + = [](SelfObject& self, int distance, int duration, sol::table luaIdle, bool repeat, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::vector idle; + // Lua index starts at 1 + for (size_t i = 1; i <= luaIdle.size(); i++) + idle.emplace_back(luaIdle.get(i)); + ai.stack(MWMechanics::AiWander(distance, duration, 0, idle, repeat), ptr, cancelOther); + }; + selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target, bool repeat, bool cancelOther) { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - int gameHoursDuration = static_cast(std::ceil(duration / 3600.0)); - ai.stack(MWMechanics::AiWander(distance, gameHoursDuration, 0, {}, false), ptr, cancelOther); + ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), repeat), ptr, cancelOther); }; - selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target, bool cancelOther) { + selfAPI["_enableLuaAnimations"] = [](SelfObject& self, bool enable) { const MWWorld::Ptr& ptr = self.ptr(); - MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), false), ptr, cancelOther); + MWBase::Environment::get().getMechanicsManager()->enableLuaAnimations(ptr, enable); }; } - LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) - : LuaUtil::ScriptsContainer(lua, "L" + obj.id().toString()) + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, LuaUtil::ScriptTracker* tracker) + : LuaUtil::ScriptsContainer(lua, "L" + obj.id().toString(), tracker, false) , mData(obj) { - this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); + lua->protectedCall( + [&](LuaUtil::LuaView& view) { addPackage("openmw.self", sol::make_object(view.sol(), &mData)); }); registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers, - &mOnTeleportedHandlers }); + &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers, &mOnSkillUse, + &mOnSkillLevelUp }); } void LocalScripts::setActive(bool active) diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 6b1555868d..b32d8bba9e 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -22,7 +22,7 @@ namespace MWLua class CachedStat { public: - using Index = std::variant; + using Index = std::variant; using Setter = void (*)(const Index&, std::string_view, const MWWorld::Ptr&, const sol::object&); CachedStat(Setter setter, Index index, std::string_view prop) @@ -62,15 +62,32 @@ namespace MWLua { public: static void initializeSelfPackage(const Context&); - LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, LuaUtil::ScriptTracker* tracker = nullptr); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } const MWWorld::Ptr& getPtrOrEmpty() const { return mData.ptrOrEmpty(); } void setActive(bool active); + bool isActive() const override { return mData.mIsActive; } void onConsume(const LObject& consumable) { callEngineHandlers(mOnConsumeHandlers, consumable); } void onActivated(const LObject& actor) { callEngineHandlers(mOnActivatedHandlers, actor); } void onTeleported() { callEngineHandlers(mOnTeleportedHandlers); } + void onAnimationTextKey(std::string_view groupname, std::string_view key) + { + callEngineHandlers(mOnAnimationTextKeyHandlers, groupname, key); + } + void onPlayAnimation(std::string_view groupname, const sol::table& options) + { + callEngineHandlers(mOnPlayAnimationHandlers, groupname, options); + } + void onSkillUse(std::string_view skillId, int useType, float scale) + { + callEngineHandlers(mOnSkillUse, skillId, useType, scale); + } + void onSkillLevelUp(std::string_view skillId, std::string_view source) + { + callEngineHandlers(mOnSkillLevelUp, skillId, source); + } void applyStatsCache(); @@ -83,6 +100,10 @@ namespace MWLua EngineHandlerList mOnConsumeHandlers{ "onConsume" }; EngineHandlerList mOnActivatedHandlers{ "onActivated" }; EngineHandlerList mOnTeleportedHandlers{ "onTeleported" }; + EngineHandlerList mOnAnimationTextKeyHandlers{ "_onAnimationTextKey" }; + EngineHandlerList mOnPlayAnimationHandlers{ "_onPlayAnimation" }; + EngineHandlerList mOnSkillUse{ "_onSkillUse" }; + EngineHandlerList mOnSkillLevelUp{ "_onSkillLevelUp" }; }; } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 65b6977982..8debbe153d 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -1,335 +1,44 @@ #include "luabindings.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include #include "../mwbase/environment.hpp" -#include "../mwbase/statemanager.hpp" -#include "../mwworld/action.hpp" -#include "../mwworld/class.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/datetimemanager.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/manualref.hpp" -#include "../mwworld/store.hpp" -#include "../mwworld/worldmodel.hpp" - -#include "luaevents.hpp" -#include "luamanagerimp.hpp" -#include "mwscriptbindings.hpp" -#include "objectlists.hpp" +#include "animationbindings.hpp" #include "camerabindings.hpp" #include "cellbindings.hpp" +#include "corebindings.hpp" #include "debugbindings.hpp" #include "inputbindings.hpp" -#include "magicbindings.hpp" +#include "localscripts.hpp" +#include "markupbindings.hpp" +#include "menuscripts.hpp" #include "nearbybindings.hpp" #include "objectbindings.hpp" #include "postprocessingbindings.hpp" #include "soundbindings.hpp" #include "types/types.hpp" #include "uibindings.hpp" +#include "vfsbindings.hpp" +#include "worldbindings.hpp" namespace MWLua { - struct CellsStore - { - }; -} - -namespace sol -{ - template <> - struct is_automagical : std::false_type - { - }; -} - -namespace MWLua -{ - - static void checkGameInitialized(LuaUtil::LuaState* lua) - { - if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) - throw std::runtime_error( - "This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback()); - } - - static void addTimeBindings(sol::table& api, const Context& context, bool global) - { - MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); - - api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); }; - api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); }; - api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); }; - api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); }; - api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); }; - api["getRealTime"] = []() { - return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); - }; - - if (!global) - return; - - api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); }; - api["setSimulationTimeScale"] = [context, timeManager](float scale) { - context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); }); - }; - - api["pause"] - = [timeManager](sol::optional tag) { timeManager->pause(tag.value_or("paused")); }; - api["unpause"] - = [timeManager](sol::optional tag) { timeManager->unpause(tag.value_or("paused")); }; - api["getPausedTags"] = [timeManager](sol::this_state lua) { - sol::table res(lua, sol::create); - for (const std::string& tag : timeManager->getPausedTags()) - res[tag] = tag; - return res; - }; - } - - static sol::table initContentFilesBindings(sol::state_view& lua) - { - const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); - sol::table list(lua, sol::create); - for (size_t i = 0; i < contentList.size(); ++i) - list[i + 1] = Misc::StringUtils::lowerCase(contentList[i]); - sol::table res(lua, sol::create); - res["list"] = LuaUtil::makeReadOnly(list); - res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional { - for (size_t i = 0; i < contentList.size(); ++i) - if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) - return i + 1; - return sol::nullopt; - }; - res["has"] = [&contentList](std::string_view contentFile) -> bool { - for (size_t i = 0; i < contentList.size(); ++i) - if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) - return true; - return false; - }; - return LuaUtil::makeReadOnly(res); - } - - static sol::table initCorePackage(const Context& context) - { - auto* lua = context.mLua; - sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 44; - api["quit"] = [lua]() { - Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); - MWBase::Environment::get().getStateManager()->requestQuit(); - }; - api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { - context.mLuaEvents->addGlobalEvent( - { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); - }; - api["contentFiles"] = initContentFilesBindings(lua->sol()); - api["sound"] = initCoreSoundBindings(context); - api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { - const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); - for (size_t i = 0; i < contentList.size(); ++i) - if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) - return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText(); - throw std::runtime_error("Content file not found: " + std::string(contentFile)); - }; - addTimeBindings(api, context, false); - api["magic"] = initCoreMagicBindings(context); - api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager()); - const MWWorld::Store* gmstStore - = &MWBase::Environment::get().getESMStore()->get(); - api["getGMST"] = [lua = context.mLua, gmstStore](const std::string& setting) -> sol::object { - const ESM::GameSetting* gmst = gmstStore->search(setting); - if (gmst == nullptr) - return sol::nil; - const ESM::Variant& value = gmst->mValue; - switch (value.getType()) - { - case ESM::VT_Float: - return sol::make_object(lua->sol(), value.getFloat()); - case ESM::VT_Short: - case ESM::VT_Long: - case ESM::VT_Int: - return sol::make_object(lua->sol(), value.getInteger()); - case ESM::VT_String: - return sol::make_object(lua->sol(), value.getString()); - case ESM::VT_Unknown: - case ESM::VT_None: - break; - } - return sol::nil; - }; - - // TODO: deprecate this and provide access to the store instead - sol::table skills(context.mLua->sol(), sol::create); - api["SKILL"] = LuaUtil::makeStrictReadOnly(skills); - for (int i = 0; i < ESM::Skill::Length; ++i) - { - ESM::RefId skill = ESM::Skill::indexToRefId(i); - std::string id = skill.serializeText(); - std::string key = Misc::StringUtils::lowerCase(skill.getRefIdString()); - // force first character to uppercase for backwards compatability - key[0] += 'A' - 'a'; - skills[key] = id; - } - - // TODO: deprecate this and provide access to the store instead - sol::table attributes(context.mLua->sol(), sol::create); - api["ATTRIBUTE"] = LuaUtil::makeStrictReadOnly(attributes); - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - ESM::RefId attribute = ESM::Attribute::indexToRefId(i); - std::string id = attribute.serializeText(); - std::string key = Misc::StringUtils::lowerCase(attribute.getRefIdString()); - // force first character to uppercase for backwards compatability - key[0] += 'A' - 'a'; - attributes[key] = id; - } - - return LuaUtil::makeReadOnly(api); - } - - static void addCellGetters(sol::table& api, const Context& context) - { - api["getCellByName"] = [](std::string_view name) { - return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) }; - }; - api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) { - ESM::RefId worldspace; - if (cellOrName.is()) - worldspace = cellOrName.as().mStore->getCell()->getWorldSpace(); - else if (cellOrName.is() && !cellOrName.as().empty()) - worldspace = MWBase::Environment::get() - .getWorldModel() - ->getCell(cellOrName.as()) - .getCell() - ->getWorldSpace(); - else - worldspace = ESM::Cell::sDefaultWorldspaceId; - return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior( - ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) }; - }; - - const MWWorld::Store* cells3Store = &MWBase::Environment::get().getESMStore()->get(); - const MWWorld::Store* cells4Store = &MWBase::Environment::get().getESMStore()->get(); - sol::usertype cells = context.mLua->sol().new_usertype("Cells"); - cells[sol::meta_function::length] - = [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); }; - cells[sol::meta_function::index] = [cells3Store, cells4Store](const CellsStore&, size_t index) -> GCell { - index--; // Translate from Lua's 1-based indexing. - if (index < cells3Store->getSize()) - { - const ESM::Cell* cellRecord = cells3Store->at(index); - return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( - cellRecord->mId, /*forceLoad=*/false) }; - } - else - { - const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize()); - return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( - cellRecord->mId, /*forceLoad=*/false) }; - } - }; - cells[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get(); - cells[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get(); - api["cells"] = CellsStore{}; - } - - static sol::table initWorldPackage(const Context& context) - { - sol::table api(context.mLua->sol(), sol::create); - ObjectLists* objectLists = context.mObjectLists; - addTimeBindings(api, context, true); - addCellGetters(api, context); - api["mwscript"] = initMWScriptBindings(context); - api["activeActors"] = GObjectList{ objectLists->getActorsInScene() }; - api["players"] = GObjectList{ objectLists->getPlayers() }; - api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional count) -> GObject { - checkGameInitialized(lua); - MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId)); - const MWWorld::Ptr& ptr = mref.getPtr(); - ptr.getRefData().disable(); - MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell(); - MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); - return GObject(newPtr); - }; - api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { - ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); - if (!refId.is()) - throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); - return GObject(*refId.getIf()); - }; - - // Creates a new record in the world database. - api["createRecord"] = sol::overload( - [lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(activator); - }, - [lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(armor); - }, - [lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(clothing); - }, - [lua = context.mLua](const ESM::Book& book) -> const ESM::Book* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(book); - }, - [lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(misc); - }, - [lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(potion); - }, - [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(weapon); - }); - - api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) { - if (!object.ptr().getRefData().activate()) - return; - context.mLuaManager->addAction( - [object, actor] { - const MWWorld::Ptr& objPtr = object.ptr(); - const MWWorld::Ptr& actorPtr = actor.ptr(); - objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr); - }, - "_runStandardActivationAction"); - }; - - return LuaUtil::makeReadOnly(api); - } - std::map initCommonPackages(const Context& context) { - sol::state_view lua = context.mLua->sol(); + sol::state_view lua = context.mLua->unsafeState(); MWWorld::DateTimeManager* tm = MWBase::Environment::get().getWorld()->getTimeManager(); return { + { "openmw.animation", initAnimationPackage(context) }, { "openmw.async", LuaUtil::getAsyncPackageInitializer( lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) }, - { "openmw.core", initCorePackage(context) }, - { "openmw.types", initTypesPackage(context) }, + { "openmw.markup", initMarkupPackage(context) }, { "openmw.util", LuaUtil::initUtilPackage(lua) }, + { "openmw.vfs", initVFSPackage(context) }, }; } @@ -338,6 +47,8 @@ namespace MWLua initObjectBindingsForGlobalScripts(context); initCellBindingsForGlobalScripts(context); return { + { "openmw.core", initCorePackage(context) }, + { "openmw.types", initTypesPackage(context) }, { "openmw.world", initWorldPackage(context) }, }; } @@ -348,6 +59,8 @@ namespace MWLua initCellBindingsForLocalScripts(context); LocalScripts::initializeSelfPackage(context); return { + { "openmw.core", initCorePackage(context) }, + { "openmw.types", initTypesPackage(context) }, { "openmw.nearby", initNearbyPackage(context) }, }; } @@ -356,7 +69,7 @@ namespace MWLua { return { { "openmw.ambient", initAmbientPackage(context) }, - { "openmw.camera", initCameraPackage(context.mLua->sol()) }, + { "openmw.camera", initCameraPackage(context.sol()) }, { "openmw.debug", initDebugPackage(context) }, { "openmw.input", initInputPackage(context) }, { "openmw.postprocessing", initPostprocessingPackage(context) }, @@ -364,4 +77,14 @@ namespace MWLua }; } + std::map initMenuPackages(const Context& context) + { + return { + { "openmw.core", initCorePackage(context) }, + { "openmw.ambient", initAmbientPackage(context) }, + { "openmw.ui", initUserInterfacePackage(context) }, + { "openmw.menu", initMenuPackage(context) }, + { "openmw.input", initInputPackage(context) }, + }; + } } diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index e5d481d1eb..749987e5b2 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -12,14 +12,18 @@ namespace MWLua // Initialize Lua packages that are available for all scripts. std::map initCommonPackages(const Context&); - // Initialize Lua packages that are available only for global scripts. + // Initialize Lua packages that are available for global scripts (additionally to common packages). std::map initGlobalPackages(const Context&); - // Initialize Lua packages that are available only for local scripts (including player scripts). + // Initialize Lua packages that are available for local scripts (additionally to common packages). std::map initLocalPackages(const Context&); - // Initialize Lua packages that are available only for local scripts on the player. + // Initialize Lua packages that are available only for local scripts on the player (additionally to common and local + // packages). std::map initPlayerPackages(const Context&); + + // Initialize Lua packages that are available only for menu scripts (additionally to common packages). + std::map initMenuPackages(const Context&); } #endif // MWLUA_LUABINDINGS_H diff --git a/apps/openmw/mwlua/luaevents.cpp b/apps/openmw/mwlua/luaevents.cpp index b036fea3b6..4ffb4fc1cc 100644 --- a/apps/openmw/mwlua/luaevents.cpp +++ b/apps/openmw/mwlua/luaevents.cpp @@ -13,6 +13,7 @@ #include "globalscripts.hpp" #include "localscripts.hpp" +#include "menuscripts.hpp" namespace MWLua { @@ -23,6 +24,7 @@ namespace MWLua mLocalEventBatch.clear(); mNewGlobalEventBatch.clear(); mNewLocalEventBatch.clear(); + mMenuEvents.clear(); } void LuaEvents::finalizeEventBatch() @@ -51,8 +53,15 @@ namespace MWLua mLocalEventBatch.clear(); } + void LuaEvents::callMenuEventHandlers() + { + for (const Global& e : mMenuEvents) + mMenuScripts.receiveEvent(e.mEventName, e.mEventData); + mMenuEvents.clear(); + } + template - static void saveEvent(ESM::ESMWriter& esm, const ESM::RefNum& dest, const Event& event) + static void saveEvent(ESM::ESMWriter& esm, ESM::RefNum dest, const Event& event) { esm.writeHNString("LUAE", event.mEventName); esm.writeFormId(dest, true); diff --git a/apps/openmw/mwlua/luaevents.hpp b/apps/openmw/mwlua/luaevents.hpp index 5eeae46538..3890b45b6d 100644 --- a/apps/openmw/mwlua/luaevents.hpp +++ b/apps/openmw/mwlua/luaevents.hpp @@ -23,12 +23,14 @@ namespace MWLua { class GlobalScripts; + class MenuScripts; class LuaEvents { public: - explicit LuaEvents(GlobalScripts& globalScripts) + explicit LuaEvents(GlobalScripts& globalScripts, MenuScripts& menuScripts) : mGlobalScripts(globalScripts) + , mMenuScripts(menuScripts) { } @@ -45,11 +47,13 @@ namespace MWLua }; void addGlobalEvent(Global event) { mNewGlobalEventBatch.push_back(std::move(event)); } + void addMenuEvent(Global event) { mMenuEvents.push_back(std::move(event)); } void addLocalEvent(Local event) { mNewLocalEventBatch.push_back(std::move(event)); } void clear(); void finalizeEventBatch(); void callEventHandlers(); + void callMenuEventHandlers(); void load(lua_State* lua, ESM::ESMReader& esm, const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer); @@ -57,10 +61,12 @@ namespace MWLua private: GlobalScripts& mGlobalScripts; + MenuScripts& mMenuScripts; std::vector mNewGlobalEventBatch; std::vector mNewLocalEventBatch; std::vector mGlobalEventBatch; std::vector mLocalEventBatch; + std::vector mMenuEvents; }; } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 7262d945c5..780ddaf9a9 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -18,14 +18,18 @@ #include #include +#include #include #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwrender/bonegroup.hpp" #include "../mwrender/postprocessor.hpp" #include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/scene.hpp" #include "../mwworld/worldmodel.hpp" @@ -62,69 +66,96 @@ namespace MWLua mGlobalScripts.setSerializer(mGlobalSerializer.get()); } + LuaManager::~LuaManager() + { + LuaUi::clearSettings(); + } + void LuaManager::initConfiguration() { mConfiguration.init(MWBase::Environment::get().getESMStore()->getLuaScriptsCfg()); Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; for (size_t i = 0; i < mConfiguration.size(); ++i) Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); + mMenuScripts.setAutoStartConf(mConfiguration.getMenuConf()); mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf()); } void LuaManager::init() { - Context context; - context.mIsGlobal = true; - context.mLuaManager = this; - context.mLua = &mLua; - context.mObjectLists = &mObjectLists; - context.mLuaEvents = &mLuaEvents; - context.mSerializer = mGlobalSerializer.get(); + mLua.protectedCall([&](LuaUtil::LuaView& view) { + Context globalContext; + globalContext.mType = Context::Global; + globalContext.mLuaManager = this; + globalContext.mLua = &mLua; + globalContext.mObjectLists = &mObjectLists; + globalContext.mLuaEvents = &mLuaEvents; + globalContext.mSerializer = mGlobalSerializer.get(); - Context localContext = context; - localContext.mIsGlobal = false; - localContext.mSerializer = mLocalSerializer.get(); + Context localContext = globalContext; + localContext.mType = Context::Local; + localContext.mSerializer = mLocalSerializer.get(); - for (const auto& [name, package] : initCommonPackages(context)) - mLua.addCommonPackage(name, package); - for (const auto& [name, package] : initGlobalPackages(context)) - mGlobalScripts.addPackage(name, package); + Context menuContext = globalContext; + menuContext.mType = Context::Menu; - mLocalPackages = initLocalPackages(localContext); - mPlayerPackages = initPlayerPackages(localContext); - mPlayerPackages.insert(mLocalPackages.begin(), mLocalPackages.end()); + for (const auto& [name, package] : initCommonPackages(globalContext)) + mLua.addCommonPackage(name, package); + for (const auto& [name, package] : initGlobalPackages(globalContext)) + mGlobalScripts.addPackage(name, package); + for (const auto& [name, package] : initMenuPackages(menuContext)) + mMenuScripts.addPackage(name, package); - LuaUtil::LuaStorage::initLuaBindings(mLua.sol()); - mGlobalScripts.addPackage( - "openmw.storage", LuaUtil::LuaStorage::initGlobalPackage(mLua.sol(), &mGlobalStorage)); - mLocalPackages["openmw.storage"] = LuaUtil::LuaStorage::initLocalPackage(mLua.sol(), &mGlobalStorage); - mPlayerPackages["openmw.storage"] - = LuaUtil::LuaStorage::initPlayerPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage); + mLocalPackages = initLocalPackages(localContext); - initConfiguration(); - mInitialized = true; + mPlayerPackages = initPlayerPackages(localContext); + mPlayerPackages.insert(mLocalPackages.begin(), mLocalPackages.end()); + + LuaUtil::LuaStorage::initLuaBindings(view); + mGlobalScripts.addPackage("openmw.storage", LuaUtil::LuaStorage::initGlobalPackage(view, &mGlobalStorage)); + mMenuScripts.addPackage( + "openmw.storage", LuaUtil::LuaStorage::initMenuPackage(view, &mGlobalStorage, &mPlayerStorage)); + mLocalPackages["openmw.storage"] = LuaUtil::LuaStorage::initLocalPackage(view, &mGlobalStorage); + mPlayerPackages["openmw.storage"] + = LuaUtil::LuaStorage::initPlayerPackage(view, &mGlobalStorage, &mPlayerStorage); + + mPlayerStorage.setActive(true); + mGlobalStorage.setActive(false); + + initConfiguration(); + mInitialized = true; + mMenuScripts.addAutoStartedScripts(); + }); } void LuaManager::loadPermanentStorage(const std::filesystem::path& userConfigPath) { + mPlayerStorage.setActive(true); + mGlobalStorage.setActive(true); const auto globalPath = userConfigPath / "global_storage.bin"; const auto playerPath = userConfigPath / "player_storage.bin"; - if (std::filesystem::exists(globalPath)) - mGlobalStorage.load(globalPath); - if (std::filesystem::exists(playerPath)) - mPlayerStorage.load(playerPath); + + mLua.protectedCall([&](LuaUtil::LuaView& view) { + if (std::filesystem::exists(globalPath)) + mGlobalStorage.load(view.sol(), globalPath); + if (std::filesystem::exists(playerPath)) + mPlayerStorage.load(view.sol(), playerPath); + }); } void LuaManager::savePermanentStorage(const std::filesystem::path& userConfigPath) { - mGlobalStorage.save(userConfigPath / "global_storage.bin"); - mPlayerStorage.save(userConfigPath / "player_storage.bin"); + mLua.protectedCall([&](LuaUtil::LuaView& view) { + if (mGlobalScriptsStarted) + mGlobalStorage.save(view.sol(), userConfigPath / "global_storage.bin"); + mPlayerStorage.save(view.sol(), userConfigPath / "player_storage.bin"); + }); } void LuaManager::update() { - if (Settings::lua().mGcStepsPerFrame > 0) - lua_gc(mLua.sol(), LUA_GCSTEP, Settings::lua().mGcStepsPerFrame); + if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0) + lua_gc(mLua.unsafeState(), LUA_GCSTEP, steps); if (mPlayer.isEmpty()) return; // The game is not started yet. @@ -140,9 +171,12 @@ namespace MWLua mObjectLists.update(); - std::erase_if(mActiveLocalScripts, [](const LocalScripts* l) { - return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().getRefData().isDeleted(); - }); + for (auto scripts : mQueuedAutoStartedScripts) + scripts->addAutoStartedScripts(); + mQueuedAutoStartedScripts.clear(); + + std::erase_if(mActiveLocalScripts, + [](const LocalScripts* l) { return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().mRef->isDeleted(); }); mGlobalScripts.statsNextFrame(); for (LocalScripts* scripts : mActiveLocalScripts) @@ -153,6 +187,7 @@ namespace MWLua MWWorld::DateTimeManager& timeManager = *MWBase::Environment::get().getWorld()->getTimeManager(); if (!timeManager.isPaused()) { + mMenuScripts.processTimers(timeManager.getSimulationTime(), timeManager.getGameTime()); mGlobalScripts.processTimers(timeManager.getSimulationTime(), timeManager.getGameTime()); for (LocalScripts* scripts : mActiveLocalScripts) scripts->processTimers(timeManager.getSimulationTime(), timeManager.getGameTime()); @@ -175,6 +210,8 @@ namespace MWLua scripts->update(frameDuration); mGlobalScripts.update(frameDuration); } + + mLua.protectedCall([&](LuaUtil::LuaView& lua) { mScriptTracker.unloadInactiveScripts(lua); }); } void LuaManager::objectTeleported(const MWWorld::Ptr& ptr) @@ -204,9 +241,11 @@ namespace MWLua void LuaManager::synchronizedUpdate() { - if (mPlayer.isEmpty()) - return; // The game is not started yet. + mLua.protectedCall([&](LuaUtil::LuaView&) { synchronizedUpdateUnsafe(); }); + } + void LuaManager::synchronizedUpdateUnsafe() + { if (mNewGameStarted) { mNewGameStarted = false; @@ -217,32 +256,56 @@ namespace MWLua // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. mProcessingInputEvents = true; - PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + PlayerScripts* playerScripts + = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + + for (const auto& event : mMenuInputEvents) + mMenuScripts.processInputEvent(event); + mMenuInputEvents.clear(); if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) { for (const auto& event : mInputEvents) playerScripts->processInputEvent(event); } mInputEvents.clear(); + mLuaEvents.callMenuEventHandlers(); + double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() + ? 0.0 + : MWBase::Environment::get().getFrameDuration(); + mInputActions.update(frameDuration); + mMenuScripts.onFrame(frameDuration); if (playerScripts) - playerScripts->onFrame(MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() - ? 0.0 - : MWBase::Environment::get().getFrameDuration()); + playerScripts->onFrame(frameDuration); mProcessingInputEvents = false; - for (const std::string& message : mUIMessages) - windowManager->messageBox(message); + for (const auto& [message, mode] : mUIMessages) + windowManager->messageBox(message, mode); mUIMessages.clear(); for (auto& [msg, color] : mInGameConsoleMessages) windowManager->printToConsole(msg, "#" + color.toHex()); mInGameConsoleMessages.clear(); applyDelayedActions(); + + if (mReloadAllScriptsRequested) + { + // Reloading right after `applyDelayedActions` to guarantee that no delayed actions are currently queued. + reloadAllScriptsImpl(); + mReloadAllScriptsRequested = false; + } + + if (mDelayedUiModeChangedArg) + { + if (playerScripts) + playerScripts->uiModeChanged(*mDelayedUiModeChangedArg, true); + mDelayedUiModeChangedArg = std::nullopt; + } } void LuaManager::applyDelayedActions() { + mApplyingDelayedActions = true; for (DelayedAction& action : mActionQueue) action.apply(); mActionQueue.clear(); @@ -250,32 +313,38 @@ namespace MWLua if (mTeleportPlayerAction) mTeleportPlayerAction->apply(); mTeleportPlayerAction.reset(); + mApplyingDelayedActions = false; } void LuaManager::clear() { - LuaUi::clearUserInterface(); + LuaUi::clearGameInterface(); mUiResourceManager.clear(); - MWBase::Environment::get().getWindowManager()->setConsoleMode(""); MWBase::Environment::get().getWorld()->getPostProcessor()->disableDynamicShaders(); mActiveLocalScripts.clear(); mLuaEvents.clear(); mEngineEvents.clear(); mInputEvents.clear(); + mMenuInputEvents.clear(); mObjectLists.clear(); mGlobalScripts.removeAllScripts(); mGlobalScriptsStarted = false; mNewGameStarted = false; + mDelayedUiModeChangedArg = std::nullopt; if (!mPlayer.isEmpty()) { mPlayer.getCellRef().unsetRefNum(); mPlayer.getRefData().setLuaScripts(nullptr); mPlayer = MWWorld::Ptr(); } + mGlobalStorage.setActive(true); mGlobalStorage.clearTemporaryAndRemoveCallbacks(); + mGlobalStorage.setActive(false); mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + mInputActions.clear(); + mInputTriggers.clear(); for (int i = 0; i < 5; ++i) - lua_gc(mLua.sol(), LUA_GCCOLLECT, 0); + lua_gc(mLua.unsafeState(), LUA_GCCOLLECT, 0); } void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) @@ -291,7 +360,7 @@ namespace MWLua if (!localScripts) { localScripts = createLocalScripts(ptr); - localScripts->addAutoStartedScripts(); + mQueuedAutoStartedScripts.push_back(localScripts); } mActiveLocalScripts.insert(localScripts); mEngineEvents.addToQueue(EngineEvents::OnActive{ getId(ptr) }); @@ -299,6 +368,7 @@ namespace MWLua void LuaManager::newGameStarted() { + mGlobalStorage.setActive(true); mInputEvents.clear(); mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; @@ -307,18 +377,108 @@ namespace MWLua void LuaManager::gameLoaded() { + mGlobalStorage.setActive(true); if (!mGlobalScriptsStarted) mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; + mMenuScripts.stateChanged(); + } + + void LuaManager::gameEnded() + { + // TODO: disable scripts and global storage when the game is actually unloaded + // mGlobalStorage.setActive(false); + mMenuScripts.stateChanged(); + } + + void LuaManager::noGame() + { + clear(); + mMenuScripts.stateChanged(); } void LuaManager::uiModeChanged(const MWWorld::Ptr& arg) { if (mPlayer.isEmpty()) return; + ObjectId argId = arg.isEmpty() ? ObjectId() : getId(arg); + if (mApplyingDelayedActions) + { + mDelayedUiModeChangedArg = argId; + return; + } PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); if (playerScripts) - playerScripts->uiModeChanged(arg); + playerScripts->uiModeChanged(argId, false); + } + + void LuaManager::actorDied(const MWWorld::Ptr& actor) + { + if (actor.isEmpty()) + return; + mLuaEvents.addLocalEvent({ getId(actor), "Died", {} }); + } + + void LuaManager::useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) + { + MWBase::Environment::get().getWorldModel()->registerPtr(object); + mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force }); + } + + void LuaManager::animationTextKey(const MWWorld::Ptr& actor, const std::string& key) + { + auto pos = key.find(": "); + if (pos != std::string::npos) + mEngineEvents.addToQueue( + EngineEvents::OnAnimationTextKey{ getId(actor), key.substr(0, pos), key.substr(pos + 2) }); + } + + void LuaManager::playAnimation(const MWWorld::Ptr& actor, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table options = view.newTable(); + options["blendMask"] = blendMask; + options["autoDisable"] = autodisable; + options["speed"] = speedmult; + options["startKey"] = start; + options["stopKey"] = stop; + options["startPoint"] = startpoint; + options["loops"] = loops; + options["forceLoop"] = loopfallback; + + bool priorityAsTable = false; + for (uint32_t i = 1; i < MWRender::sNumBlendMasks; i++) + if (priority[static_cast(i)] != priority[static_cast(0)]) + priorityAsTable = true; + if (priorityAsTable) + { + sol::table priorityTable = view.newTable(); + for (uint32_t i = 0; i < MWRender::sNumBlendMasks; i++) + priorityTable[static_cast(i)] = priority[static_cast(i)]; + options["priority"] = priorityTable; + } + else + options["priority"] = priority[MWRender::BoneGroup_LowerBody]; + + // mEngineEvents.addToQueue(event); + // Has to be called immediately, otherwise engine details that depend on animations playing immediately + // break. + if (auto* scripts = actor.getRefData().getLuaScripts()) + scripts->onPlayAnimation(groupname, options); + }); + } + + void LuaManager::skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) + { + mEngineEvents.addToQueue(EngineEvents::OnSkillUse{ getId(actor), skillId.serializeText(), useType, scale }); + } + + void LuaManager::skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) + { + mEngineEvents.addToQueue( + EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) }); } void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) @@ -334,7 +494,7 @@ namespace MWLua if (!autoStartConf.empty()) { localScripts = createLocalScripts(ptr, std::move(autoStartConf)); - localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()` + mQueuedAutoStartedScripts.push_back(localScripts); } } if (localScripts) @@ -360,6 +520,7 @@ namespace MWLua { mInputEvents.push_back(event); } + mMenuInputEvents.push_back(event); } MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const @@ -400,7 +561,7 @@ namespace MWLua } else { - scripts = std::make_shared(&mLua, LObject(getId(ptr))); + scripts = std::make_shared(&mLua, LObject(getId(ptr)), &mScriptTracker); if (!autoStartConf.has_value()) autoStartConf = mConfiguration.getLocalConf(type, ptr.getCellRef().getRefId(), getId(ptr)); scripts->setAutoStartConf(std::move(*autoStartConf)); @@ -441,9 +602,15 @@ namespace MWLua throw std::runtime_error("Last generated RefNum is invalid"); MWBase::Environment::get().getWorldModel()->setLastGeneratedRefNum(lastGenerated); + // TODO: don't execute scripts right away, it will be necessary in multiplayer where global storage requires + // initialization. For now just set global storage as active slightly before it would be set by gameLoaded() + mGlobalStorage.setActive(true); + ESM::LuaScripts globalScripts; globalScripts.load(reader); - mLuaEvents.load(mLua.sol(), reader, mContentFileMapping, mGlobalLoader.get()); + mLua.protectedCall([&](LuaUtil::LuaView& view) { + mLuaEvents.load(view.sol(), reader, mContentFileMapping, mGlobalLoader.get()); + }); mGlobalScripts.setSavedDataDeserializer(mGlobalLoader.get()); mGlobalScripts.load(globalScripts); @@ -473,42 +640,66 @@ namespace MWLua scripts->setSerializer(mLocalSerializer.get()); scripts->setSavedDataDeserializer(mLocalLoader.get()); scripts->load(data); - - // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. - MWBase::Environment::get().getWorldModel()->deregisterPtr(ptr); } - void LuaManager::reloadAllScripts() + void LuaManager::reloadAllScriptsImpl() { Log(Debug::Info) << "Reload Lua"; - LuaUi::clearUserInterface(); + LuaUi::clearGameInterface(); + LuaUi::clearMenuInterface(); + LuaUi::clearSettings(); MWBase::Environment::get().getWindowManager()->setConsoleMode(""); MWBase::Environment::get().getL10nManager()->dropCache(); mUiResourceManager.clear(); mLua.dropScriptCache(); + mInputActions.clear(); + mInputTriggers.clear(); initConfiguration(); - { // Reload global scripts + ESM::LuaScripts globalData; + + if (mGlobalScriptsStarted) + { mGlobalScripts.setSavedDataDeserializer(mGlobalSerializer.get()); - ESM::LuaScripts data; - mGlobalScripts.save(data); + mGlobalScripts.save(globalData); mGlobalStorage.clearTemporaryAndRemoveCallbacks(); - mGlobalScripts.load(data); } + std::unordered_map localData; + for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView()) - { // Reload local scripts + { LocalScripts* scripts = ptr.getRefData().getLuaScripts(); if (scripts == nullptr) continue; scripts->setSavedDataDeserializer(mLocalSerializer.get()); ESM::LuaScripts data; scripts->save(data); - scripts->load(data); + localData[id] = std::move(data); } + + mMenuScripts.removeAllScripts(); + + mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + + mMenuScripts.addAutoStartedScripts(); + + for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView()) + { + LocalScripts* scripts = ptr.getRefData().getLuaScripts(); + if (scripts == nullptr) + continue; + scripts->load(localData[id]); + } + for (LocalScripts* scripts : mActiveLocalScripts) scripts->setActive(true); + + if (mGlobalScriptsStarted) + { + mGlobalScripts.load(globalData); + } } void LuaManager::handleConsoleCommand( @@ -517,16 +708,18 @@ namespace MWLua PlayerScripts* playerScripts = nullptr; if (!mPlayer.isEmpty()) playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); - if (!playerScripts) + bool processed = mMenuScripts.consoleCommand(consoleMode, command); + if (playerScripts) { - MWBase::Environment::get().getWindowManager()->printToConsole( - "You must enter a game session to run Lua commands\n", MWBase::WindowManager::sConsoleColor_Error); - return; + sol::object selected = sol::nil; + if (!selectedPtr.isEmpty()) + mLua.protectedCall([&](LuaUtil::LuaView& view) { + selected = sol::make_object(view.sol(), LObject(getId(selectedPtr))); + }); + if (playerScripts->consoleCommand(consoleMode, command, selected)) + processed = true; } - sol::object selected = sol::nil; - if (!selectedPtr.isEmpty()) - selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr))); - if (!playerScripts->consoleCommand(consoleMode, command, selected)) + if (!processed) MWBase::Environment::get().getWindowManager()->printToConsole( "No Lua handlers for console\n", MWBase::WindowManager::sConsoleColor_Error); } @@ -558,6 +751,8 @@ namespace MWLua void LuaManager::addAction(std::function action, std::string_view name) { + if (mApplyingDelayedActions) + throw std::runtime_error("DelayedAction is not allowed to create another DelayedAction"); mActionQueue.emplace_back(&mLua, std::move(action), name); } @@ -658,10 +853,11 @@ namespace MWLua for (size_t i = 0; i < mConfiguration.size(); ++i) { bool isGlobal = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sGlobal; + bool isMenu = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sMenu; out << std::left; - out << " " << std::setw(nameW) << mConfiguration[i].mScriptPath; - if (mConfiguration[i].mScriptPath.size() > nameW) + out << " " << std::setw(nameW) << mConfiguration[i].mScriptPath.value(); + if (mConfiguration[i].mScriptPath.value().size() > nameW) out << "\n " << std::setw(nameW) << ""; // if path is too long, break line out << std::right; out << std::setw(valueW) << static_cast(activeStats[i].mAvgInstructionCount); @@ -670,13 +866,12 @@ namespace MWLua if (isGlobal) out << std::setw(valueW * 2) << "NA (global script)"; + else if (isMenu && (!selectedScripts || !selectedScripts->hasScript(i))) + out << std::setw(valueW * 2) << "NA (menu script)"; else if (selectedPtr.isEmpty()) out << std::setw(valueW * 2) << "NA (not selected) "; else if (!selectedScripts || !selectedScripts->hasScript(i)) - { - out << std::setw(valueW) << "-"; - outMemSize(selectedStats[i].mMemoryUsage); - } + out << std::setw(valueW * 2) << "NA"; else { out << std::setw(valueW) << static_cast(selectedStats[i].mAvgInstructionCount); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index d00fda9dda..3f2135e9c9 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -6,17 +6,21 @@ #include #include +#include #include +#include #include #include #include #include "../mwbase/luamanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "engineevents.hpp" #include "globalscripts.hpp" #include "localscripts.hpp" #include "luaevents.hpp" +#include "menuscripts.hpp" #include "object.hpp" #include "objectlists.hpp" @@ -33,6 +37,7 @@ namespace MWLua LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir); LuaManager(const LuaManager&) = delete; LuaManager(LuaManager&&) = delete; + ~LuaManager(); // Called by engine.cpp when the environment is fully initialized. void init(); @@ -66,6 +71,8 @@ namespace MWLua // LuaManager queues these events and propagates to scripts on the next `update` call. void newGameStarted() override; void gameLoaded() override; + void gameEnded() override; + void noGame() override; void objectAddedToScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void inputEvent(const InputEvent& event) override; @@ -77,6 +84,14 @@ namespace MWLua { mEngineEvents.addToQueue(EngineEvents::OnActivate{ getId(actor), getId(object) }); } + void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override; + void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) override; + void playAnimation(const MWWorld::Ptr& actor, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, uint32_t loops, + bool loopfallback) override; + void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override; + void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); @@ -84,6 +99,7 @@ namespace MWLua void objectTeleported(const MWWorld::Ptr& ptr) override; void questUpdated(const ESM::RefId& questId, int stage) override; void uiModeChanged(const MWWorld::Ptr& arg) override; + void actorDied(const MWWorld::Ptr& actor) override; MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; @@ -92,7 +108,11 @@ namespace MWLua // Used only in Lua bindings void addCustomLocalScript(const MWWorld::Ptr&, int scriptId, std::string_view initData); - void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } + void addUIMessage( + std::string_view message, MWGui::ShowInDialogueMode mode = MWGui::ShowInDialogueMode_IfPossible) + { + mUIMessages.emplace_back(message, mode); + } void addInGameConsoleMessage(const std::string& msg, const Misc::Color& color) { mInGameConsoleMessages.push_back({ msg, color }); @@ -112,8 +132,9 @@ namespace MWLua void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) override; void setContentFileMapping(const std::map& mapping) override { mContentFileMapping = mapping; } - // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. - void reloadAllScripts() override; + // At the end of the next `synchronizedUpdate` drops script cache and reloads all scripts. + // Calls `onSave` and `onLoad` for every script. + void reloadAllScripts() override { mReloadAllScriptsRequested = true; } void handleConsoleCommand( const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) override; @@ -130,8 +151,9 @@ namespace MWLua template std::function wrapLuaCallback(const LuaUtil::Callback& c) { - return - [this, c](Arg arg) { this->queueCallback(c, sol::main_object(this->mLua.sol(), sol::in_place, arg)); }; + return [this, c](Arg arg) { + this->queueCallback(c, sol::main_object(this->mLua.unsafeState(), sol::in_place, arg)); + }; } LuaUi::ResourceManager* uiResourceManager() { return &mUiResourceManager; } @@ -141,30 +163,40 @@ namespace MWLua void reportStats(unsigned int frameNumber, osg::Stats& stats) const; std::string formatResourceUsageStats() const override; + LuaUtil::InputAction::Registry& inputActions() { return mInputActions; } + LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; } + private: void initConfiguration(); LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, std::optional autoStartConf = std::nullopt); + void reloadAllScriptsImpl(); + void synchronizedUpdateUnsafe(); bool mInitialized = false; bool mGlobalScriptsStarted = false; bool mProcessingInputEvents = false; + bool mApplyingDelayedActions = false; bool mNewGameStarted = false; + bool mReloadAllScriptsRequested = false; LuaUtil::ScriptsConfiguration mConfiguration; LuaUtil::LuaState mLua; LuaUi::ResourceManager mUiResourceManager; std::map mLocalPackages; std::map mPlayerPackages; + MenuScripts mMenuScripts{ &mLua }; GlobalScripts mGlobalScripts{ &mLua }; std::set mActiveLocalScripts; + std::vector mQueuedAutoStartedScripts; ObjectLists mObjectLists; MWWorld::Ptr mPlayer; - LuaEvents mLuaEvents{ mGlobalScripts }; + LuaEvents mLuaEvents{ mGlobalScripts, mMenuScripts }; EngineEvents mEngineEvents{ mGlobalScripts }; std::vector mInputEvents; + std::vector mMenuInputEvents; std::unique_ptr mGlobalSerializer; std::unique_ptr mLocalSerializer; @@ -194,11 +226,17 @@ namespace MWLua }; std::vector mActionQueue; std::optional mTeleportPlayerAction; - std::vector mUIMessages; + std::vector> mUIMessages; std::vector> mInGameConsoleMessages; + std::optional mDelayedUiModeChangedArg; - LuaUtil::LuaStorage mGlobalStorage{ mLua.sol() }; - LuaUtil::LuaStorage mPlayerStorage{ mLua.sol() }; + LuaUtil::LuaStorage mGlobalStorage; + LuaUtil::LuaStorage mPlayerStorage; + + LuaUtil::InputAction::Registry mInputActions; + LuaUtil::InputTrigger::Registry mInputTriggers; + + LuaUtil::ScriptTracker mScriptTracker; }; } diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index d1dab50574..7259d03f4c 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -11,25 +11,30 @@ #include #include #include +#include #include #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/activespells.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwworld/worldmodel.hpp" #include "localscripts.hpp" #include "luamanagerimp.hpp" #include "object.hpp" #include "objectvariant.hpp" +#include "recordstore.hpp" namespace MWLua { @@ -134,16 +139,12 @@ namespace MWLua namespace sol { - template - struct is_automagical> : std::false_type - { - }; template <> struct is_automagical : std::false_type { }; template <> - struct is_automagical : std::false_type + struct is_automagical : std::false_type { }; template <> @@ -191,43 +192,58 @@ namespace MWLua return ESM::RefId::deserializeText(LuaUtil::cast(recordOrId)); } + static const ESM::Spell* toSpell(const sol::object& spellOrId) + { + if (spellOrId.is()) + return spellOrId.as(); + else + { + auto& store = MWBase::Environment::get().getWorld()->getStore(); + auto refId = ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + return store.get().find(refId); + } + } + + static sol::table effectParamsListToTable(lua_State* lua, const std::vector& effects) + { + sol::table res(lua, sol::create); + for (size_t i = 0; i < effects.size(); ++i) + res[LuaUtil::toLuaIndex(i)] = effects[i]; // ESM::IndexedENAMstruct (effect params) + return res; + } + sol::table initCoreMagicBindings(const Context& context) { - sol::state_view& lua = context.mLua->sol(); + sol::state_view lua = context.sol(); sol::table magicApi(lua, sol::create); // Constants - magicApi["RANGE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Self", ESM::RT_Self }, - { "Touch", ESM::RT_Touch }, - { "Target", ESM::RT_Target }, - })); - magicApi["SCHOOL"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Alteration", 0 }, - { "Conjuration", 1 }, - { "Destruction", 2 }, - { "Illusion", 3 }, - { "Mysticism", 4 }, - { "Restoration", 5 }, - })); + magicApi["RANGE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Self", ESM::RT_Self }, + { "Touch", ESM::RT_Touch }, + { "Target", ESM::RT_Target }, + })); magicApi["SPELL_TYPE"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Spell", ESM::Spell::ST_Spell }, - { "Ability", ESM::Spell::ST_Ability }, - { "Blight", ESM::Spell::ST_Blight }, - { "Disease", ESM::Spell::ST_Disease }, - { "Curse", ESM::Spell::ST_Curse }, - { "Power", ESM::Spell::ST_Power }, - })); + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Spell", ESM::Spell::ST_Spell }, + { "Ability", ESM::Spell::ST_Ability }, + { "Blight", ESM::Spell::ST_Blight }, + { "Disease", ESM::Spell::ST_Disease }, + { "Curse", ESM::Spell::ST_Curse }, + { "Power", ESM::Spell::ST_Power }, + })); magicApi["ENCHANTMENT_TYPE"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "CastOnce", ESM::Enchantment::Type::CastOnce }, - { "CastOnStrike", ESM::Enchantment::Type::WhenStrikes }, - { "CastOnUse", ESM::Enchantment::Type::WhenUsed }, - { "ConstantEffect", ESM::Enchantment::Type::ConstantEffect }, - })); + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "CastOnce", ESM::Enchantment::Type::CastOnce }, + { "CastOnStrike", ESM::Enchantment::Type::WhenStrikes }, + { "CastOnUse", ESM::Enchantment::Type::WhenUsed }, + { "ConstantEffect", ESM::Enchantment::Type::ConstantEffect }, + })); - sol::table effect(context.mLua->sol(), sol::create); + sol::table effect(lua, sol::create); magicApi["EFFECT_TYPE"] = LuaUtil::makeStrictReadOnly(effect); for (const auto& name : ESM::MagicEffect::sIndexNames) { @@ -235,42 +251,18 @@ namespace MWLua } // Spell store - using SpellStore = MWWorld::Store; - const SpellStore* spellStore = &MWBase::Environment::get().getWorld()->getStore().get(); - sol::usertype spellStoreT = lua.new_usertype("ESM3_SpellStore"); - spellStoreT[sol::meta_function::to_string] - = [](const SpellStore& store) { return "ESM3_SpellStore{" + std::to_string(store.getSize()) + " spells}"; }; - spellStoreT[sol::meta_function::length] = [](const SpellStore& store) { return store.getSize(); }; - spellStoreT[sol::meta_function::index] = sol::overload( - [](const SpellStore& store, size_t index) -> const ESM::Spell* { return store.at(index - 1); }, - [](const SpellStore& store, std::string_view spellId) -> const ESM::Spell* { - return store.find(ESM::RefId::deserializeText(spellId)); - }); - spellStoreT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); - spellStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); - - magicApi["spells"] = spellStore; + sol::table spells(lua, sol::create); + addRecordFunctionBinding(spells, context); + magicApi["spells"] = LuaUtil::makeReadOnly(spells); // Enchantment store - using EnchantmentStore = MWWorld::Store; - const EnchantmentStore* enchantmentStore - = &MWBase::Environment::get().getWorld()->getStore().get(); - sol::usertype enchantmentStoreT = lua.new_usertype("ESM3_EnchantmentStore"); - enchantmentStoreT[sol::meta_function::to_string] = [](const EnchantmentStore& store) { - return "ESM3_EnchantmentStore{" + std::to_string(store.getSize()) + " enchantments}"; - }; - enchantmentStoreT[sol::meta_function::length] = [](const EnchantmentStore& store) { return store.getSize(); }; - enchantmentStoreT[sol::meta_function::index] = sol::overload( - [](const EnchantmentStore& store, size_t index) -> const ESM::Enchantment* { return store.at(index - 1); }, - [](const EnchantmentStore& store, std::string_view enchantmentId) -> const ESM::Enchantment* { - return store.find(ESM::RefId::deserializeText(enchantmentId)); - }); - enchantmentStoreT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); - enchantmentStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); - - magicApi["enchantments"] = enchantmentStore; + sol::table enchantments(lua, sol::create); + addRecordFunctionBinding(enchantments, context); + magicApi["enchantments"] = LuaUtil::makeReadOnly(enchantments); // MagicEffect store + sol::table magicEffects(lua, sol::create); + magicApi["effects"] = LuaUtil::makeReadOnly(magicEffects); using MagicEffectStore = MWWorld::Store; const MagicEffectStore* magicEffectStore = &MWBase::Environment::get().getWorld()->getStore().get(); @@ -279,10 +271,10 @@ namespace MWLua return "ESM3_MagicEffectStore{" + std::to_string(store.getSize()) + " effects}"; }; magicEffectStoreT[sol::meta_function::index] = sol::overload( - [](const MagicEffectStore& store, int id) -> const ESM::MagicEffect* { return store.find(id); }, + [](const MagicEffectStore& store, int id) -> const ESM::MagicEffect* { return store.search(id); }, [](const MagicEffectStore& store, std::string_view id) -> const ESM::MagicEffect* { int index = ESM::MagicEffect::indexNameToIndex(id); - return store.find(index); + return store.search(index); }); auto magicEffectsIter = [magicEffectStore](sol::this_state lua, const sol::object& /*store*/, sol::optional id) -> std::tuple { @@ -302,8 +294,10 @@ namespace MWLua }; magicEffectStoreT[sol::meta_function::pairs] = [iter = sol::make_object(lua, magicEffectsIter)] { return iter; }; + magicEffectStoreT[sol::meta_function::ipairs] + = [iter = sol::make_object(lua, magicEffectsIter)] { return iter; }; - magicApi["effects"] = magicEffectStore; + magicEffects["records"] = magicEffectStore; // Spell record auto spellT = lua.new_usertype("ESM3_Spell"); @@ -313,11 +307,14 @@ namespace MWLua spellT["name"] = sol::readonly_property([](const ESM::Spell& rec) -> std::string_view { return rec.mName; }); spellT["type"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mType; }); spellT["cost"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mCost; }); - spellT["effects"] = sol::readonly_property([&lua](const ESM::Spell& rec) -> sol::table { - sol::table res(lua, sol::create); - for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) - res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) - return res; + spellT["alwaysSucceedFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Always); }); + spellT["starterSpellFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_PCStart); }); + spellT["autocalcFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Autocalc); }); + spellT["effects"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Spell& rec) -> sol::table { + return effectParamsListToTable(lua, rec.mEffects.mList); }); // Enchantment record @@ -332,50 +329,54 @@ namespace MWLua enchantT["cost"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCost; }); enchantT["charge"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; }); - enchantT["effects"] = sol::readonly_property([&lua](const ESM::Enchantment& rec) -> sol::table { - sol::table res(lua, sol::create); - for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) - res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) - return res; - }); + enchantT["effects"] + = sol::readonly_property([lua = lua.lua_state()](const ESM::Enchantment& rec) -> sol::table { + return effectParamsListToTable(lua, rec.mEffects.mList); + }); // Effect params - auto effectParamsT = lua.new_usertype("ESM3_EffectParams"); - effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::ENAMstruct& params) { - const ESM::MagicEffect* const rec = magicEffectStore->find(params.mEffectID); + auto effectParamsT = lua.new_usertype("ESM3_EffectParams"); + effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::IndexedENAMstruct& params) { + const ESM::MagicEffect* const rec = magicEffectStore->find(params.mData.mEffectID); return "ESM3_EffectParams[" + ESM::MagicEffect::indexToGmstString(rec->mIndex) + "]"; }; - effectParamsT["effect"] - = sol::readonly_property([magicEffectStore](const ESM::ENAMstruct& params) -> const ESM::MagicEffect* { - return magicEffectStore->find(params.mEffectID); - }); + effectParamsT["effect"] = sol::readonly_property( + [magicEffectStore](const ESM::IndexedENAMstruct& params) -> const ESM::MagicEffect* { + return magicEffectStore->find(params.mData.mEffectID); + }); + effectParamsT["id"] = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> std::string { + auto name = ESM::MagicEffect::indexToName(params.mData.mEffectID); + return Misc::StringUtils::lowerCase(name); + }); effectParamsT["affectedSkill"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional { - ESM::RefId id = ESM::Skill::indexToRefId(params.mSkill); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional { + ESM::RefId id = ESM::Skill::indexToRefId(params.mData.mSkill); if (!id.empty()) return id.serializeText(); return sol::nullopt; }); effectParamsT["affectedAttribute"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional { - ESM::RefId id = ESM::Attribute::indexToRefId(params.mAttribute); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional { + ESM::RefId id = ESM::Attribute::indexToRefId(params.mData.mAttribute); if (!id.empty()) return id.serializeText(); return sol::nullopt; }); effectParamsT["range"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mRange; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mRange; }); effectParamsT["area"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mArea; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mArea; }); effectParamsT["magnitudeMin"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMin; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMin; }); effectParamsT["magnitudeMax"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMax; }); - effectParamsT["duration"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mDuration; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMax; }); + effectParamsT["duration"] = sol::readonly_property( + [](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mDuration; }); + effectParamsT["index"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mIndex; }); // MagicEffect record - auto magicEffectT = context.mLua->sol().new_usertype("ESM3_MagicEffect"); + auto magicEffectT = lua.new_usertype("ESM3_MagicEffect"); magicEffectT[sol::meta_function::to_string] = [](const ESM::MagicEffect& rec) { return "ESM3_MagicEffect[" + ESM::MagicEffect::indexToGmstString(rec.mIndex) + "]"; @@ -388,6 +389,27 @@ namespace MWLua auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); + magicEffectT["particle"] + = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return rec.mParticle; }); + magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool { + return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + }); + magicEffectT["areaSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mAreaSound.serializeText(); }); + magicEffectT["boltSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mBoltSound.serializeText(); }); + magicEffectT["castSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCastSound.serializeText(); }); + magicEffectT["hitSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHitSound.serializeText(); }); + magicEffectT["areaStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); }); + magicEffectT["bolt"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mBolt.serializeText(); }); + magicEffectT["castStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); }); + magicEffectT["hitStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); }); magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return MWBase::Environment::get() .getWorld() @@ -397,20 +419,32 @@ namespace MWLua ->mValue.getString(); }); magicEffectT["school"] = sol::readonly_property( - [](const ESM::MagicEffect& rec) -> int { return ESM::MagicSchool::skillRefIdToIndex(rec.mData.mSchool); }); + [](const ESM::MagicEffect& rec) -> std::string { return rec.mData.mSchool.serializeText(); }); magicEffectT["baseCost"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mBaseCost; }); magicEffectT["color"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> Misc::Color { return Misc::Color(rec.mData.mRed / 255.f, rec.mData.mGreen / 255.f, rec.mData.mBlue / 255.f, 1.f); }); + magicEffectT["hasDuration"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoDuration); }); + magicEffectT["hasMagnitude"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoMagnitude); }); + // TODO: Not self-explanatory. Needs either a better name or documentation. The description in + // loadmgef.hpp is uninformative. + magicEffectT["isAppliedOnce"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::AppliedOnce; }); magicEffectT["harmful"] = sol::readonly_property( [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::Harmful; }); + magicEffectT["casterLinked"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::CasterLinked; }); + magicEffectT["nonRecastable"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::NonRecastable; }); // TODO: Should we expose it? What happens if a spell has several effects with different projectileSpeed? // magicEffectT["projectileSpeed"] // = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mSpeed; }); - auto activeSpellEffectT = context.mLua->sol().new_usertype("ActiveSpellEffect"); + auto activeSpellEffectT = lua.new_usertype("ActiveSpellEffect"); activeSpellEffectT[sol::meta_function::to_string] = [](const ESM::ActiveEffect& effect) { return "ActiveSpellEffect[" + ESM::MagicEffect::indexToGmstString(effect.mEffectId) + "]"; }; @@ -418,6 +452,8 @@ namespace MWLua auto name = ESM::MagicEffect::indexToName(effect.mEffectId); return Misc::StringUtils::lowerCase(name); }); + activeSpellEffectT["index"] + = sol::readonly_property([](const ESM::ActiveEffect& effect) -> int { return effect.mEffectIndex; }); activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string { return MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()).toString(); }); @@ -479,52 +515,71 @@ namespace MWLua return effect.mDuration; }); - auto activeSpellT = context.mLua->sol().new_usertype("ActiveSpellParams"); + auto activeSpellT = lua.new_usertype("ActiveSpellParams"); activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) { - return "ActiveSpellParams[" + activeSpell.mParams.getId().serializeText() + "]"; + return "ActiveSpellParams[" + activeSpell.mParams.getSourceSpellId().serializeText() + "]"; }; activeSpellT["name"] = sol::readonly_property( [](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); }); - activeSpellT["id"] = sol::readonly_property( - [](const ActiveSpell& activeSpell) -> std::string { return activeSpell.mParams.getId().serializeText(); }); - activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object { - auto item = activeSpell.mParams.getItem(); - if (!item.isSet()) - return sol::nil; - auto itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(item); - if (itemPtr.isEmpty()) - return sol::nil; - if (activeSpell.mActor.isGObject()) - return sol::make_object(lua, GObject(itemPtr)); - else - return sol::make_object(lua, LObject(itemPtr)); + activeSpellT["id"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string { + return activeSpell.mParams.getSourceSpellId().serializeText(); }); - activeSpellT["caster"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object { - auto caster - = MWBase::Environment::get().getWorld()->searchPtrViaActorId(activeSpell.mParams.getCasterActorId()); - if (caster.isEmpty()) - return sol::nil; - else - { - if (activeSpell.mActor.isGObject()) - return sol::make_object(lua, GObject(getId(caster))); - else - return sol::make_object(lua, LObject(getId(caster))); - } + activeSpellT["item"] + = sol::readonly_property([lua = lua.lua_state()](const ActiveSpell& activeSpell) -> sol::object { + auto item = activeSpell.mParams.getItem(); + if (!item.isSet()) + return sol::nil; + auto itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(item); + if (itemPtr.isEmpty()) + return sol::nil; + if (activeSpell.mActor.isGObject()) + return sol::make_object(lua, GObject(itemPtr)); + else + return sol::make_object(lua, LObject(itemPtr)); + }); + activeSpellT["caster"] + = sol::readonly_property([lua = lua.lua_state()](const ActiveSpell& activeSpell) -> sol::object { + auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( + activeSpell.mParams.getCasterActorId()); + if (caster.isEmpty()) + return sol::nil; + else + { + if (activeSpell.mActor.isGObject()) + return sol::make_object(lua, GObject(getId(caster))); + else + return sol::make_object(lua, LObject(getId(caster))); + } + }); + activeSpellT["effects"] + = sol::readonly_property([lua = lua.lua_state()](const ActiveSpell& activeSpell) -> sol::table { + sol::table res(lua, sol::create); + size_t tableIndex = 0; + for (const ESM::ActiveEffect& effect : activeSpell.mParams.getEffects()) + { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + res[++tableIndex] = effect; // ESM::ActiveEffect (effect params) + } + return res; + }); + activeSpellT["fromEquipment"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Equipment); }); - activeSpellT["effects"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::table { - sol::table res(lua, sol::create); - size_t tableIndex = 0; - for (const ESM::ActiveEffect& effect : activeSpell.mParams.getEffects()) - { - if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) - continue; - res[++tableIndex] = effect; // ESM::ActiveEffect (effect params) - } - return res; + activeSpellT["temporary"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Temporary); + }); + activeSpellT["affectsBaseValues"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues); + }); + activeSpellT["stackable"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Stackable); + }); + activeSpellT["activeSpellId"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string { + return activeSpell.mParams.getActiveSpellId().serializeText(); }); - auto activeEffectT = context.mLua->sol().new_usertype("ActiveEffect"); + auto activeEffectT = lua.new_usertype("ActiveEffect"); activeEffectT[sol::meta_function::to_string] = [](const ActiveEffect& effect) { return "ActiveEffect[" + ESM::MagicEffect::indexToGmstString(effect.key.mId) + "]"; @@ -561,25 +616,98 @@ namespace MWLua return LuaUtil::makeReadOnly(magicApi); } + static std::pair> getNameAndMagicEffects( + const MWWorld::Ptr& actor, ESM::RefId id, const sol::table& effects, bool quiet) + { + std::vector effectIndexes; + + for (const auto& entry : effects) + { + if (entry.second.is()) + effectIndexes.push_back(entry.second.as()); + else if (entry.second.is()) + throw std::runtime_error("Error: Adding effects as enam structs is not implemented, use indexes."); + else + throw std::runtime_error("Unexpected entry in 'effects' table while trying to add to active effects"); + } + + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + + auto getEffectsFromIndexes = [&](const ESM::EffectList& effects) { + std::vector enams; + for (auto index : effectIndexes) + enams.push_back(effects.mList.at(index)); + return enams; + }; + + auto getNameAndEffects = [&](auto* record) { + return std::pair>( + record->mName, getEffectsFromIndexes(record->mEffects)); + }; + auto getNameAndEffectsEnch = [&](auto* record) { + auto* enchantment = esmStore.get().find(record->mEnchant); + return std::pair>( + record->mName, getEffectsFromIndexes(enchantment->mEffects)); + }; + switch (esmStore.find(id)) + { + case ESM::REC_ALCH: + return getNameAndEffects(esmStore.get().find(id)); + case ESM::REC_INGR: + { + // Ingredients are a special case as their effect list is calculated on consumption. + const ESM::Ingredient* ingredient = esmStore.get().find(id); + std::vector enams; + quiet = quiet || actor != MWMechanics::getPlayer(); + for (uint32_t i = 0; i < effectIndexes.size(); i++) + { + if (auto effect = MWMechanics::rollIngredientEffect(actor, ingredient, effectIndexes[i])) + enams.push_back(effect->mList[0]); + } + if (enams.empty() && !quiet) + { + // "X has no effect on you" + std::string message = esmStore.get().find("sNotifyMessage50")->mValue.getString(); + message = Misc::StringUtils::format(message, ingredient->mName); + MWBase::Environment::get().getWindowManager()->messageBox(message); + } + return { ingredient->mName, std::move(enams) }; + } + case ESM::REC_ARMO: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_BOOK: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_CLOT: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_WEAP: + return getNameAndEffectsEnch(esmStore.get().find(id)); + default: + // esmStore.find doesn't find REC_SPELs + case ESM::REC_SPEL: + return getNameAndEffects(esmStore.get().find(id)); + } + } + void addActorMagicBindings(sol::table& actor, const Context& context) { + auto lua = context.sol(); const MWWorld::Store* spellStore = &MWBase::Environment::get().getWorld()->getStore().get(); // types.Actor.spells(o) actor["spells"] = [](const sol::object& actor) { return ActorSpells{ actor }; }; - auto spellsT = context.mLua->sol().new_usertype("ActorSpells"); + auto spellsT = lua.new_usertype("ActorSpells"); spellsT[sol::meta_function::to_string] = [](const ActorSpells& spells) { return "ActorSpells[" + spells.mActor.object().toString() + "]"; }; actor["activeSpells"] = [](const sol::object& actor) { return ActorActiveSpells{ actor }; }; - auto activeSpellsT = context.mLua->sol().new_usertype("ActorActiveSpells"); + auto activeSpellsT = lua.new_usertype("ActorActiveSpells"); activeSpellsT[sol::meta_function::to_string] = [](const ActorActiveSpells& spells) { return "ActorActiveSpells[" + spells.mActor.object().toString() + "]"; }; actor["activeEffects"] = [](const sol::object& actor) { return ActorActiveEffects{ actor }; }; - auto activeEffectsT = context.mLua->sol().new_usertype("ActorActiveEffects"); + auto activeEffectsT = lua.new_usertype("ActorActiveEffects"); activeEffectsT[sol::meta_function::to_string] = [](const ActorActiveEffects& effects) { return "ActorActiveEffects[" + effects.mActor.object().toString() + "]"; }; @@ -615,17 +743,55 @@ namespace MWLua context.mLuaManager->addAction([obj = Object(ptr), spellId]() { const MWWorld::Ptr& ptr = obj.ptr(); auto& stats = ptr.getClass().getCreatureStats(ptr); + + // We need to deselect any enchant items before we can select a spell otherwise the item will be + // reselected + const auto resetEnchantItem = [&]() { + if (ptr.getClass().hasInventoryStore(ptr)) + { + MWWorld::InventoryStore& inventory = ptr.getClass().getInventoryStore(ptr); + inventory.setSelectedEnchantItem(inventory.end()); + } + }; + + if (spellId.empty()) + { + resetEnchantItem(); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); + else + stats.getSpells().setSelectedSpell(ESM::RefId()); + return; + } if (!stats.getSpells().hasSpell(spellId)) throw std::runtime_error("Actor doesn't know spell " + spellId.toDebugString()); + + resetEnchantItem(); if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) { - int chance = 0; - if (!spellId.empty()) - chance = MWMechanics::getSpellSuccessChance(spellId, ptr); + int chance = MWMechanics::getSpellSuccessChance(spellId, ptr); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, chance); } else - ptr.getClass().getCreatureStats(ptr).getSpells().setSelectedSpell(spellId); + stats.getSpells().setSelectedSpell(spellId); + }); + }; + + actor["clearSelectedCastable"] = [context](const SelfObject& o) { + if (!o.ptr().getClass().isActor()) + throw std::runtime_error("Actor expected"); + context.mLuaManager->addAction([obj = Object(o.ptr())]() { + const MWWorld::Ptr& ptr = obj.ptr(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + if (ptr.getClass().hasInventoryStore(ptr)) + { + MWWorld::InventoryStore& inventory = ptr.getClass().getInventoryStore(ptr); + inventory.setSelectedEnchantItem(inventory.end()); + } + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); + else + stats.getSpells().setSelectedSpell(ESM::RefId()); }); }; @@ -638,36 +804,36 @@ namespace MWLua // types.Actor.spells(o)[i] spellsT[sol::meta_function::index] = sol::overload( - [](const ActorSpells& spells, size_t index) -> sol::optional { + [](const ActorSpells& spells, size_t index) -> const ESM::Spell* { if (auto* store = spells.getStore()) - if (index <= store->count()) - return store->at(index - 1); - return sol::nullopt; + if (index <= store->count() && index > 0) + return store->at(LuaUtil::fromLuaIndex(index)); + return nullptr; }, - [spellStore](const ActorSpells& spells, std::string_view spellId) -> sol::optional { + [spellStore](const ActorSpells& spells, std::string_view spellId) -> const ESM::Spell* { if (auto* store = spells.getStore()) { - const ESM::Spell* spell = spellStore->find(ESM::RefId::deserializeText(spellId)); - if (store->hasSpell(spell)) + const ESM::Spell* spell = spellStore->search(ESM::RefId::deserializeText(spellId)); + if (spell && store->hasSpell(spell)) return spell; } - return sol::nullopt; + return nullptr; }); // pairs(types.Actor.spells(o)) - spellsT[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get(); + spellsT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); // ipairs(types.Actor.spells(o)) - spellsT[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get(); + spellsT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); // types.Actor.spells(o):add(id) spellsT["add"] = [context](const ActorSpells& spells, const sol::object& spellOrId) { if (spells.mActor.isLObject()) throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); - context.mLuaManager->addAction([obj = spells.mActor.object(), id = toSpellId(spellOrId)]() { + context.mLuaManager->addAction([obj = spells.mActor.object(), spell = toSpell(spellOrId)]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) - ptr.getClass().getCreatureStats(ptr).getSpells().add(id); + ptr.getClass().getCreatureStats(ptr).getSpells().add(spell, false); }); }; @@ -675,10 +841,10 @@ namespace MWLua spellsT["remove"] = [context](const ActorSpells& spells, const sol::object& spellOrId) { if (spells.mActor.isLObject()) throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); - context.mLuaManager->addAction([obj = spells.mActor.object(), id = toSpellId(spellOrId)]() { + context.mLuaManager->addAction([obj = spells.mActor.object(), spell = toSpell(spellOrId)]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) - ptr.getClass().getCreatureStats(ptr).getSpells().remove(id); + ptr.getClass().getCreatureStats(ptr).getSpells().remove(spell, false); }); }; @@ -693,6 +859,16 @@ namespace MWLua }); }; + // types.Actor.spells(o):canUsePower() + spellsT["canUsePower"] = [](const ActorSpells& spells, const sol::object& spellOrId) -> bool { + if (spells.mActor.isLObject()) + throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); + auto* spell = toSpell(spellOrId); + if (auto* store = spells.getStore()) + return store->canUsePower(spell); + return false; + }; + // pairs(types.Actor.activeSpells(o)) activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) { sol::state_view lua(ts); @@ -700,10 +876,10 @@ namespace MWLua return sol::as_function([lua, self]() mutable -> std::pair { if (!self.isEnd()) { - auto id = sol::make_object(lua, self.mIterator->getId().serializeText()); + auto id = sol::make_object(lua, self.mIterator->getSourceSpellId().serializeText()); auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator }); self.advance(); - return { params, params }; + return { id, params }; } else { @@ -724,14 +900,97 @@ namespace MWLua }; // types.Actor.activeSpells(o):remove(id) - activeSpellsT["remove"] = [](const ActorActiveSpells& spells, const sol::object& spellOrId) { + activeSpellsT["remove"] = [context](const ActorActiveSpells& spells, std::string_view idStr) { + if (spells.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + context.mLuaManager->addAction([spells = spells, id = ESM::RefId::deserializeText(idStr)]() { + if (auto* store = spells.getStore()) + { + auto it = store->getActiveSpellById(id); + if (it != store->end()) + { + if (it->hasFlag(ESM::ActiveSpells::Flag_Temporary)) + store->removeEffectsByActiveSpellId(spells.mActor.ptr(), id); + else + throw std::runtime_error("Can only remove temporary effects."); + } + } + }); + }; + + // types.Actor.activeSpells(o):add(id, spellid, effects, options) + activeSpellsT["add"] = [](const ActorActiveSpells& spells, const sol::table& options) { if (spells.isLObject()) throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); - auto id = toSpellId(spellOrId); if (auto* store = spells.getStore()) { - store->removeEffects(spells.mActor.ptr(), id); + ESM::RefId id = ESM::RefId::deserializeText(options.get("id")); + sol::optional item = options.get>("item"); + ESM::RefNum itemId; + if (item) + itemId = item->id(); + sol::optional caster = options.get>("caster"); + bool stackable = options.get_or("stackable", false); + bool ignoreReflect = options.get_or("ignoreReflect", false); + bool ignoreSpellAbsorption = options.get_or("ignoreSpellAbsorption", false); + bool ignoreResistances = options.get_or("ignoreResistances", false); + sol::table effects = options.get("effects"); + bool quiet = options.get_or("quiet", false); + if (effects.empty()) + throw std::runtime_error("Error: Parameter 'effects': cannot be an empty list/table"); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + auto [name, enams] = getNameAndMagicEffects(spells.mActor.ptr(), id, effects, quiet); + name = options.get_or("name", name); + + MWWorld::Ptr casterPtr; + if (caster) + casterPtr = caster->ptrOrEmpty(); + + bool affectsHealth = false; + MWMechanics::ActiveSpells::ActiveSpellParams params(casterPtr, id, name, itemId); + params.setFlag(ESM::ActiveSpells::Flag_Lua); + params.setFlag(ESM::ActiveSpells::Flag_Temporary); + if (stackable) + params.setFlag(ESM::ActiveSpells::Flag_Stackable); + + for (const ESM::IndexedENAMstruct& enam : enams) + { + const ESM::MagicEffect* mgef = esmStore.get().find(enam.mData.mEffectID); + MWMechanics::ActiveSpells::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mEffectIndex = enam.mIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if (ignoreReflect) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; + if (ignoreSpellAbsorption) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + if (ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + + bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; + + bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); + effect.mTimeLeft = effect.mDuration; + params.getEffects().emplace_back(effect); + + affectsHealth = affectsHealth || mgef->mData.mFlags & ESM::MagicEffect::Harmful + || effect.mEffectId == ESM::MagicEffect::RestoreHealth; + } + store->addSpell(params); + if (affectsHealth && casterPtr == MWMechanics::getPlayer()) + // If player is attempting to cast a harmful spell on or is healing a living target, show the + // target's HP bar. + // TODO: This should be moved to Lua once the HUD has been dehardcoded + MWBase::Environment::get().getWindowManager()->setEnemy(spells.mActor.ptr()); } }; @@ -742,8 +1001,13 @@ namespace MWLua sol::state_view lua(ts); self.reset(); return sol::as_function([lua, self]() mutable -> std::pair { - if (!self.isEnd()) + while (!self.isEnd()) { + if (self.mIterator->second.getBase() == 0 && self.mIterator->second.getModifier() == 0.f) + { + self.advance(); + continue; + } ActiveEffect effect = ActiveEffect{ self.mIterator->first, self.mIterator->second }; auto result = sol::make_object(lua, effect); @@ -751,10 +1015,7 @@ namespace MWLua self.advance(); return { key, result }; } - else - { - return { sol::lua_nil, sol::lua_nil }; - } + return { sol::lua_nil, sol::lua_nil }; }); }; @@ -796,7 +1057,7 @@ namespace MWLua if (auto* store = effects.getStore()) if (auto effect = store->get(key)) return ActiveEffect{ key, effect.value() }; - return sol::nullopt; + return ActiveEffect{ key, MWMechanics::EffectParam() }; }; // types.Actor.activeEffects(o):removeEffect(id, ?arg) @@ -812,13 +1073,10 @@ namespace MWLua // Note that, although this is member method of ActorActiveEffects and we are removing an effect (not a // spell), we still need to use the active spells store to purge this effect from active spells. - auto ptr = effects.mActor.ptr(); + const auto& ptr = effects.mActor.ptr(); auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); activeSpells.purgeEffect(ptr, key.mId, key.mArg); - - // Now remove any leftover effects that have been added by script/console. - effects.getStore()->remove(key); }; // types.Actor.activeEffects(o):set(value, id, ?arg) diff --git a/apps/openmw/mwlua/markupbindings.cpp b/apps/openmw/mwlua/markupbindings.cpp new file mode 100644 index 0000000000..9a3142cc3b --- /dev/null +++ b/apps/openmw/mwlua/markupbindings.cpp @@ -0,0 +1,31 @@ +#include "markupbindings.hpp" + +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "context.hpp" + +namespace MWLua +{ + sol::table initMarkupPackage(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + api["loadYaml"] = [lua, vfs](std::string_view fileName) { + Files::IStreamPtr file = vfs->get(VFS::Path::Normalized(fileName)); + return LuaUtil::loadYaml(*file, lua); + }; + api["decodeYaml"] + = [lua](std::string_view inputData) { return LuaUtil::loadYaml(std::string(inputData), lua); }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/markupbindings.hpp b/apps/openmw/mwlua/markupbindings.hpp new file mode 100644 index 0000000000..9105ab5edf --- /dev/null +++ b/apps/openmw/mwlua/markupbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_MARKUPBINDINGS_H +#define MWLUA_MARKUPBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initMarkupPackage(const Context&); +} + +#endif // MWLUA_MARKUPBINDINGS_H diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp new file mode 100644 index 0000000000..32520c0822 --- /dev/null +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -0,0 +1,126 @@ +#include "menuscripts.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwstate/character.hpp" + +namespace MWLua +{ + static const MWState::Character* findCharacter(std::string_view characterDir) + { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it) + if (it->getPath().filename() == characterDir) + return &*it; + return nullptr; + } + + static const MWState::Slot* findSlot(const MWState::Character* character, std::string_view slotName) + { + if (!character) + return nullptr; + for (const MWState::Slot& slot : *character) + if (slot.mPath.filename() == slotName) + return &slot; + return nullptr; + } + + sol::table initMenuPackage(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + api["STATE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "NoGame", MWBase::StateManager::State_NoGame }, + { "Running", MWBase::StateManager::State_Running }, + { "Ended", MWBase::StateManager::State_Ended }, + })); + + api["getState"] = []() -> int { return MWBase::Environment::get().getStateManager()->getState(); }; + + api["newGame"] = []() { MWBase::Environment::get().getStateManager()->requestNewGame(); }; + + api["loadGame"] = [](std::string_view dir, std::string_view slotName) { + const MWState::Character* character = findCharacter(dir); + const MWState::Slot* slot = findSlot(character, slotName); + if (!slot) + throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName)); + MWBase::Environment::get().getStateManager()->requestLoad(slot->mPath); + }; + + api["deleteGame"] = [](std::string_view dir, std::string_view slotName) { + const MWState::Character* character = findCharacter(dir); + const MWState::Slot* slot = findSlot(character, slotName); + if (!slot) + throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName)); + MWBase::Environment::get().getStateManager()->deleteGame(character, slot); + }; + + api["getCurrentSaveDir"] = []() -> sol::optional { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + const MWState::Character* character = manager->getCurrentCharacter(); + if (character) + return character->getPath().filename().string(); + else + return sol::nullopt; + }; + + api["saveGame"] = [](std::string_view description, sol::optional slotName) { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + const MWState::Character* character = manager->getCurrentCharacter(); + const MWState::Slot* slot = nullptr; + if (slotName) + slot = findSlot(character, *slotName); + manager->saveGame(description, slot); + }; + + auto getSaves = [](sol::state_view lua, const MWState::Character& character) { + sol::table saves(lua, sol::create); + for (const MWState::Slot& slot : character) + { + sol::table slotInfo(lua, sol::create); + slotInfo["description"] = slot.mProfile.mDescription; + slotInfo["playerName"] = slot.mProfile.mPlayerName; + slotInfo["playerLevel"] = slot.mProfile.mPlayerLevel; + slotInfo["timePlayed"] = slot.mProfile.mTimePlayed; + sol::table contentFiles(lua, sol::create); + for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i) + contentFiles[LuaUtil::toLuaIndex(i)] = Misc::StringUtils::lowerCase(slot.mProfile.mContentFiles[i]); + + { + auto system_time = std::chrono::system_clock::now() + - (std::filesystem::file_time_type::clock::now() - slot.mTimeStamp); + slotInfo["creationTime"] = std::chrono::duration(system_time.time_since_epoch()).count(); + } + + slotInfo["contentFiles"] = contentFiles; + saves[slot.mPath.filename().string()] = slotInfo; + } + return saves; + }; + + api["getSaves"] = [getSaves](sol::this_state lua, std::string_view dir) -> sol::table { + const MWState::Character* character = findCharacter(dir); + if (!character) + throw std::runtime_error("Saves not found: " + std::string(dir)); + return getSaves(lua, *character); + }; + + api["getAllSaves"] = [getSaves](sol::this_state lua) -> sol::table { + sol::table saves(lua, sol::create); + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it) + saves[it->getPath().filename().string()] = getSaves(lua, *it); + return saves; + }; + + api["quit"] = []() { MWBase::Environment::get().getStateManager()->requestQuit(); }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp new file mode 100644 index 0000000000..8721224413 --- /dev/null +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -0,0 +1,57 @@ +#ifndef MWLUA_MENUSCRIPTS_H +#define MWLUA_MENUSCRIPTS_H + +#include + +#include +#include +#include + +#include "../mwbase/luamanager.hpp" + +#include "context.hpp" +#include "inputprocessor.hpp" + +namespace MWLua +{ + + sol::table initMenuPackage(const Context& context); + + class MenuScripts : public LuaUtil::ScriptsContainer + { + public: + MenuScripts(LuaUtil::LuaState* lua) + : LuaUtil::ScriptsContainer(lua, "Menu") + , mInputProcessor(this) + { + registerEngineHandlers({ &mOnFrameHandlers, &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged }); + } + + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + mInputProcessor.processInputEvent(event); + } + + void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } + + void stateChanged() { callEngineHandlers(mStateChanged); } + + bool consoleCommand(const std::string& consoleMode, const std::string& command) + { + callEngineHandlers(mConsoleCommandHandlers, consoleMode, command); + return !mConsoleCommandHandlers.mList.empty(); + } + + void uiModeChanged() { callEngineHandlers(mUiModeChanged); } + + private: + friend class MWLua::InputProcessor; + MWLua::InputProcessor mInputProcessor; + EngineHandlerList mOnFrameHandlers{ "onFrame" }; + EngineHandlerList mStateChanged{ "onStateChanged" }; + EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; + EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; + }; +} + +#endif // MWLUA_GLOBALSCRIPTS_H diff --git a/apps/openmw/mwlua/mwscriptbindings.cpp b/apps/openmw/mwlua/mwscriptbindings.cpp index dbe02a9fed..90d24b39fe 100644 --- a/apps/openmw/mwlua/mwscriptbindings.cpp +++ b/apps/openmw/mwlua/mwscriptbindings.cpp @@ -1,12 +1,16 @@ #include "mwscriptbindings.hpp" +#include #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/world.hpp" #include "../mwscript/globalscripts.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/worldimp.hpp" #include "object.hpp" @@ -26,6 +30,16 @@ namespace MWLua else return MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(mId); } + bool isRunning() const + { + if (mObj.has_value()) // local script + { + MWWorld::LocalScripts& localScripts = MWBase::Environment::get().getWorld()->getLocalScripts(); + return localScripts.isRunning(mId, mObj->ptr()); + } + + return MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning(mId); + } }; struct MWScriptVariables { @@ -43,14 +57,46 @@ namespace sol struct is_automagical : std::false_type { }; + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua { + float getGlobalVariableValue(const std::string_view globalId) + { + char varType = MWBase::Environment::get().getWorld()->getGlobalVariableType(globalId); + if (varType == 'f') + { + return MWBase::Environment::get().getWorld()->getGlobalFloat(globalId); + } + else if (varType == 's' || varType == 'l') + { + return static_cast(MWBase::Environment::get().getWorld()->getGlobalInt(globalId)); + } + return 0; + } + + void setGlobalVariableValue(const std::string_view globalId, float value) + { + char varType = MWBase::Environment::get().getWorld()->getGlobalVariableType(globalId); + if (varType == 'f') + { + MWBase::Environment::get().getWorld()->setGlobalFloat(globalId, value); + } + else if (varType == 's' || varType == 'l') + { + MWBase::Environment::get().getWorld()->setGlobalInt(globalId, value); + } + } + sol::table initMWScriptBindings(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); api["getGlobalScript"] = [](std::string_view recordId, sol::optional player) -> sol::optional { @@ -76,11 +122,11 @@ namespace MWLua // api["getGlobalScripts"] = [](std::string_view recordId) -> list of scripts // api["getLocalScripts"] = [](const GObject& obj) -> list of scripts - sol::usertype mwscript = context.mLua->sol().new_usertype("MWScript"); - sol::usertype mwscriptVars - = context.mLua->sol().new_usertype("MWScriptVariables"); + sol::usertype mwscript = lua.new_usertype("MWScript"); + sol::usertype mwscriptVars = lua.new_usertype("MWScriptVariables"); mwscript[sol::meta_function::to_string] = [](const MWScriptRef& s) { return std::string("MWScript{") + s.mId.toDebugString() + "}"; }; + mwscript["isRunning"] = sol::readonly_property([](const MWScriptRef& s) { return s.isRunning(); }); mwscript["recordId"] = sol::readonly_property([](const MWScriptRef& s) { return s.mId.serializeText(); }); mwscript["variables"] = sol::readonly_property([](const MWScriptRef& s) { return MWScriptVariables{ s }; }); mwscript["object"] = sol::readonly_property([](const MWScriptRef& s) -> sol::optional { @@ -98,17 +144,140 @@ namespace MWLua }); mwscript["player"] = sol::readonly_property( [](const MWScriptRef&) { return GObject(MWBase::Environment::get().getWorld()->getPlayerPtr()); }); - mwscriptVars[sol::meta_function::index] = [](MWScriptVariables& s, std::string_view var) { - return s.mRef.getLocals().getVarAsDouble(s.mRef.mId, Misc::StringUtils::lowerCase(var)); - }; - mwscriptVars[sol::meta_function::new_index] = [](MWScriptVariables& s, std::string_view var, double val) { - MWScript::Locals& locals = s.mRef.getLocals(); - if (!locals.setVar(s.mRef.mId, Misc::StringUtils::lowerCase(var), val)) - throw std::runtime_error( - "No variable \"" + std::string(var) + "\" in mwscript " + s.mRef.mId.toDebugString()); + mwscriptVars[sol::meta_function::length] + = [](MWScriptVariables& s) { return s.mRef.getLocals().getSize(s.mRef.mId); }; + mwscriptVars[sol::meta_function::index] = sol::overload( + [](MWScriptVariables& s, std::string_view var) -> sol::optional { + if (s.mRef.getLocals().hasVar(s.mRef.mId, var)) + return s.mRef.getLocals().getVarAsDouble(s.mRef.mId, Misc::StringUtils::lowerCase(var)); + else + return sol::nullopt; + }, + [](MWScriptVariables& s, std::size_t index) -> sol::optional { + auto& locals = s.mRef.getLocals(); + if (index < 1 || locals.getSize(s.mRef.mId) < index) + return sol::nullopt; + if (index <= locals.mShorts.size()) + return locals.mShorts[index - 1]; + index -= locals.mShorts.size(); + if (index <= locals.mLongs.size()) + return locals.mLongs[index - 1]; + index -= locals.mLongs.size(); + if (index <= locals.mFloats.size()) + return locals.mFloats[index - 1]; + return sol::nullopt; + }); + mwscriptVars[sol::meta_function::new_index] = sol::overload( + [](MWScriptVariables& s, std::string_view var, double val) { + MWScript::Locals& locals = s.mRef.getLocals(); + if (!locals.setVar(s.mRef.mId, Misc::StringUtils::lowerCase(var), val)) + throw std::runtime_error( + "No variable \"" + std::string(var) + "\" in mwscript " + s.mRef.mId.toDebugString()); + }, + [](MWScriptVariables& s, std::size_t index, double val) { + auto& locals = s.mRef.getLocals(); + if (index < 1 || locals.getSize(s.mRef.mId) < index) + throw std::runtime_error("Index out of range in mwscript " + s.mRef.mId.toDebugString()); + if (index <= locals.mShorts.size()) + { + locals.mShorts[index - 1] = static_cast(val); + return; + } + index -= locals.mShorts.size(); + if (index <= locals.mLongs.size()) + { + locals.mLongs[index - 1] = static_cast(val); + return; + } + index -= locals.mLongs.size(); + if (index <= locals.mFloats.size()) + locals.mFloats[index - 1] = static_cast(val); + }); + mwscriptVars[sol::meta_function::pairs] = [](MWScriptVariables& s) { + std::size_t index = 0; + const auto& compilerLocals = MWBase::Environment::get().getScriptManager()->getLocals(s.mRef.mId); + auto& locals = s.mRef.getLocals(); + std::size_t size = locals.getSize(s.mRef.mId); + return sol::as_function( + [&, index, size](sol::this_state ts) mutable -> sol::optional> { + if (index >= size) + return sol::nullopt; + auto i = index++; + if (i < locals.mShorts.size()) + return std::make_tuple(compilerLocals.get('s')[i], locals.mShorts[i]); + i -= locals.mShorts.size(); + if (i < locals.mLongs.size()) + return std::make_tuple(compilerLocals.get('l')[i], locals.mLongs[i]); + i -= locals.mLongs.size(); + if (i < locals.mFloats.size()) + return std::make_tuple(compilerLocals.get('f')[i], locals.mFloats[i]); + return sol::nullopt; + }); }; + using GlobalStore = MWWorld::Store; + sol::usertype globalStoreT = lua.new_usertype("ESM3_GlobalStore"); + const GlobalStore* globalStore = &MWBase::Environment::get().getWorld()->getStore().get(); + globalStoreT[sol::meta_function::to_string] = [](const GlobalStore& store) { + return "ESM3_GlobalStore{" + std::to_string(store.getSize()) + " globals}"; + }; + globalStoreT[sol::meta_function::length] = [](const GlobalStore& store) { return store.getSize(); }; + globalStoreT[sol::meta_function::index] = sol::overload( + [](const GlobalStore& store, std::string_view globalId) -> sol::optional { + auto g = store.search(ESM::RefId::deserializeText(globalId)); + if (g == nullptr) + return sol::nullopt; + return getGlobalVariableValue(globalId); + }, + [](const GlobalStore& store, size_t index) -> sol::optional { + if (index < 1 || store.getSize() < index) + return sol::nullopt; + auto g = store.at(LuaUtil::fromLuaIndex(index)); + if (g == nullptr) + return sol::nullopt; + std::string globalId = g->mId.serializeText(); + return getGlobalVariableValue(globalId); + }); + globalStoreT[sol::meta_function::new_index] = sol::overload( + [](const GlobalStore& store, std::string_view globalId, float val) -> void { + auto g = store.search(ESM::RefId::deserializeText(globalId)); + if (g == nullptr) + throw std::runtime_error("No variable \"" + std::string(globalId) + "\" in GlobalStore"); + setGlobalVariableValue(globalId, val); + }, + [](const GlobalStore& store, size_t index, float val) { + if (index < 1 || store.getSize() < index) + return; + auto g = store.at(LuaUtil::fromLuaIndex(index)); + if (g == nullptr) + return; + std::string globalId = g->mId.serializeText(); + setGlobalVariableValue(globalId, val); + }); + globalStoreT[sol::meta_function::pairs] = [](const GlobalStore& store) { + size_t index = 0; + return sol::as_function( + [index, &store](sol::this_state ts) mutable -> sol::optional> { + if (index >= store.getSize()) + return sol::nullopt; + + const ESM::Global* global = store.at(index++); + if (!global) + return sol::nullopt; + + std::string globalId = global->mId.serializeText(); + float value = getGlobalVariableValue(globalId); + + return std::make_tuple(globalId, value); + }); + }; + globalStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + api["getGlobalVariables"] = [globalStore](sol::optional player) { + if (player.has_value() && player->ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("First argument must either be a player or be missing"); + + return globalStore; + }; return LuaUtil::makeReadOnly(api); } - } diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index 86c8ef31e8..df317ffeba 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -3,15 +3,44 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwphysics/raycasting.hpp" +#include "../mwworld/cell.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/scene.hpp" #include "luamanagerimp.hpp" #include "objectlists.hpp" +namespace +{ + template + std::vector parseIgnoreList(const sol::table& options) + { + std::vector ignore; + + if (const auto& ignoreObj = options.get>("ignore")) + { + ignore.push_back(ignoreObj->ptr()); + } + else if (const auto& ignoreTable = options.get>("ignore")) + { + ignoreTable->for_each([&](const auto& _, const sol::object& value) { + if (value.is()) + { + ignore.push_back(value.as().ptr()); + } + }); + } + + return ignore; + } +} + namespace sol { template <> @@ -24,11 +53,12 @@ namespace MWLua { sol::table initNearbyPackage(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); ObjectLists* objectLists = context.mObjectLists; sol::usertype rayResult - = context.mLua->sol().new_usertype("RayCastingResult"); + = lua.new_usertype("RayCastingResult"); rayResult["hit"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) { return r.mHit; }); rayResult["hitPos"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional { @@ -53,38 +83,42 @@ namespace MWLua }); api["COLLISION_TYPE"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "World", MWPhysics::CollisionType_World }, - { "Door", MWPhysics::CollisionType_Door }, - { "Actor", MWPhysics::CollisionType_Actor }, - { "HeightMap", MWPhysics::CollisionType_HeightMap }, - { "Projectile", MWPhysics::CollisionType_Projectile }, - { "Water", MWPhysics::CollisionType_Water }, - { "Default", MWPhysics::CollisionType_Default }, - { "AnyPhysical", MWPhysics::CollisionType_AnyPhysical }, - { "Camera", MWPhysics::CollisionType_CameraOnly }, - { "VisualOnly", MWPhysics::CollisionType_VisualOnly }, - })); + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "World", MWPhysics::CollisionType_World }, + { "Door", MWPhysics::CollisionType_Door }, + { "Actor", MWPhysics::CollisionType_Actor }, + { "HeightMap", MWPhysics::CollisionType_HeightMap }, + { "Projectile", MWPhysics::CollisionType_Projectile }, + { "Water", MWPhysics::CollisionType_Water }, + { "Default", MWPhysics::CollisionType_Default }, + { "AnyPhysical", MWPhysics::CollisionType_AnyPhysical }, + { "Camera", MWPhysics::CollisionType_CameraOnly }, + { "VisualOnly", MWPhysics::CollisionType_VisualOnly }, + })); api["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) { - MWWorld::Ptr ignore; + std::vector ignore; int collisionType = MWPhysics::CollisionType_Default; float radius = 0; if (options) { - sol::optional ignoreObj = options->get>("ignore"); - if (ignoreObj) - ignore = ignoreObj->ptr(); + ignore = parseIgnoreList(*options); collisionType = options->get>("collisionType").value_or(collisionType); radius = options->get>("radius").value_or(0); } const MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); if (radius <= 0) - return rayCasting->castRay(from, to, ignore, std::vector(), collisionType); + { + return rayCasting->castRay(from, to, ignore, {}, collisionType); + } else { - if (!ignore.isEmpty()) - throw std::logic_error("Currently castRay doesn't support `ignore` when radius > 0"); + for (const auto& ptr : ignore) + { + if (!ptr.isEmpty()) + throw std::logic_error("Currently castRay doesn't support `ignore` when radius > 0"); + } return rayCasting->castSphere(from, to, radius, collisionType); } }; @@ -104,24 +138,40 @@ namespace MWLua // and use this callback from the main thread at the beginning of the next frame processing. rayCasting->asyncCastRay(callback, from, to, ignore, std::vector(), collisionType); };*/ - api["castRenderingRay"] = [manager = context.mLuaManager](const osg::Vec3f& from, const osg::Vec3f& to) { + api["castRenderingRay"] = [manager = context.mLuaManager](const osg::Vec3f& from, const osg::Vec3f& to, + const sol::optional& options) { if (!manager->isProcessingInputEvents()) { throw std::logic_error( "castRenderingRay can be used only in player scripts during processing of input events; " "use asyncCastRenderingRay instead."); } + + std::vector ignore; + if (options.has_value()) + { + ignore = parseIgnoreList(*options); + } + MWPhysics::RayCastingResult res; - MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false); + MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false, ignore); return res; }; - api["asyncCastRenderingRay"] = [context]( - const sol::table& callback, const osg::Vec3f& from, const osg::Vec3f& to) { - context.mLuaManager->addAction([context, callback = LuaUtil::Callback::fromLua(callback), from, to] { - MWPhysics::RayCastingResult res; - MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false); - context.mLuaManager->queueCallback(callback, sol::main_object(context.mLua->sol(), sol::in_place, res)); - }); + api["asyncCastRenderingRay"] = [context](const sol::table& callback, const osg::Vec3f& from, + const osg::Vec3f& to, const sol::optional& options) { + std::vector ignore; + if (options.has_value()) + { + ignore = parseIgnoreList(*options); + } + + context.mLuaManager->addAction( + [context, ignore = std::move(ignore), callback = LuaUtil::Callback::fromLua(callback), from, to] { + MWPhysics::RayCastingResult res; + MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false, ignore); + context.mLuaManager->queueCallback( + callback, sol::main_object(context.mLua->unsafeState(), sol::in_place, res)); + }); }; api["getObjectByFormId"] = [](std::string_view formIdStr) -> LObject { @@ -139,32 +189,35 @@ namespace MWLua api["players"] = LObjectList{ objectLists->getPlayers() }; api["NAVIGATOR_FLAGS"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Walk", DetourNavigator::Flag_walk }, - { "Swim", DetourNavigator::Flag_swim }, - { "OpenDoor", DetourNavigator::Flag_openDoor }, - { "UsePathgrid", DetourNavigator::Flag_usePathgrid }, - })); + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Walk", DetourNavigator::Flag_walk }, + { "Swim", DetourNavigator::Flag_swim }, + { "OpenDoor", DetourNavigator::Flag_openDoor }, + { "UsePathgrid", DetourNavigator::Flag_usePathgrid }, + })); api["COLLISION_SHAPE_TYPE"] = LuaUtil::makeStrictReadOnly( - context.mLua->tableFromPairs({ - { "Aabb", DetourNavigator::CollisionShapeType::Aabb }, - { "RotatingBox", DetourNavigator::CollisionShapeType::RotatingBox }, - { "Cylinder", DetourNavigator::CollisionShapeType::Cylinder }, - })); + LuaUtil::tableFromPairs(lua, + { + { "Aabb", DetourNavigator::CollisionShapeType::Aabb }, + { "RotatingBox", DetourNavigator::CollisionShapeType::RotatingBox }, + { "Cylinder", DetourNavigator::CollisionShapeType::Cylinder }, + })); api["FIND_PATH_STATUS"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Success", DetourNavigator::Status::Success }, - { "PartialPath", DetourNavigator::Status::PartialPath }, - { "NavMeshNotFound", DetourNavigator::Status::NavMeshNotFound }, - { "StartPolygonNotFound", DetourNavigator::Status::StartPolygonNotFound }, - { "EndPolygonNotFound", DetourNavigator::Status::EndPolygonNotFound }, - { "MoveAlongSurfaceFailed", DetourNavigator::Status::MoveAlongSurfaceFailed }, - { "FindPathOverPolygonsFailed", DetourNavigator::Status::FindPathOverPolygonsFailed }, - { "InitNavMeshQueryFailed", DetourNavigator::Status::InitNavMeshQueryFailed }, - { "FindStraightPathFailed", DetourNavigator::Status::FindStraightPathFailed }, - })); + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Success", DetourNavigator::Status::Success }, + { "PartialPath", DetourNavigator::Status::PartialPath }, + { "NavMeshNotFound", DetourNavigator::Status::NavMeshNotFound }, + { "StartPolygonNotFound", DetourNavigator::Status::StartPolygonNotFound }, + { "EndPolygonNotFound", DetourNavigator::Status::EndPolygonNotFound }, + { "MoveAlongSurfaceFailed", DetourNavigator::Status::MoveAlongSurfaceFailed }, + { "FindPathOverPolygonsFailed", DetourNavigator::Status::FindPathOverPolygonsFailed }, + { "InitNavMeshQueryFailed", DetourNavigator::Status::InitNavMeshQueryFailed }, + { "FindStraightPathFailed", DetourNavigator::Status::FindStraightPathFailed }, + })); static const DetourNavigator::AgentBounds defaultAgentBounds{ Settings::game().mActorCollisionShapeType, @@ -174,7 +227,7 @@ namespace MWLua | DetourNavigator::Flag_swim | DetourNavigator::Flag_openDoor | DetourNavigator::Flag_usePathgrid; api["findPath"] - = [](const osg::Vec3f& source, const osg::Vec3f& destination, const sol::optional& options) { + = [lua](const osg::Vec3f& source, const osg::Vec3f& destination, const sol::optional& options) { DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::AreaCosts areaCosts{}; @@ -206,13 +259,15 @@ namespace MWLua destinationTolerance = *v; } - std::vector result; + std::vector path; - const DetourNavigator::Status status = DetourNavigator::findPath( - *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, - includeFlags, areaCosts, destinationTolerance, std::back_inserter(result)); + const DetourNavigator::Status status + = DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, + source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); - return std::make_tuple(status, std::move(result)); + sol::table result(lua, sol::create); + LuaUtil::copyVectorToTable(path, result); + return std::make_tuple(status, result); }; api["findRandomPointAroundCircle"] = [](const osg::Vec3f& position, float maxRadius, @@ -262,6 +317,39 @@ namespace MWLua *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, from, to, includeFlags); }; + api["findNearestNavMeshPosition"] = [](const osg::Vec3f& position, const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + std::optional searchAreaHalfExtents; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("searchAreaHalfExtents")) + searchAreaHalfExtents = *v; + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + } + + if (!searchAreaHalfExtents.has_value()) + { + const bool isEsm4 = MWBase::Environment::get().getWorldScene()->getCurrentCell()->getCell()->isEsm4(); + const float halfExtents = isEsm4 + ? (1 + 2 * Constants::ESM4CellGridRadius) * Constants::ESM4CellSizeInUnits + : (1 + 2 * Constants::CellGridRadius) * Constants::CellSizeInUnits; + searchAreaHalfExtents = osg::Vec3f(halfExtents, halfExtents, halfExtents); + } + + return DetourNavigator::findNearestNavMeshPosition(*MWBase::Environment::get().getWorld()->getNavigator(), + agentBounds, position, *searchAreaHalfExtents, includeFlags); + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp index fcf225de85..d032515314 100644 --- a/apps/openmw/mwlua/object.hpp +++ b/apps/openmw/mwlua/object.hpp @@ -16,7 +16,7 @@ namespace MWLua // ObjectId is a unique identifier of a game object. // It can change only if the order of content files was change. using ObjectId = ESM::RefNum; - inline const ObjectId& getId(const MWWorld::Ptr& ptr) + inline ObjectId getId(const MWWorld::Ptr& ptr) { return ptr.getCellRef().getRefNum(); } @@ -71,6 +71,12 @@ namespace MWLua { Obj mObj; }; + + template + struct Owner + { + Obj mObj; + }; } #endif // MWLUA_OBJECT_H diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 32bd0b95d4..6575a719f6 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/localscripts.hpp" #include "../mwworld/player.hpp" #include "../mwworld/scene.hpp" #include "../mwworld/worldmodel.hpp" @@ -19,6 +21,9 @@ #include "../mwmechanics/creaturestats.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + #include "luaevents.hpp" #include "luamanagerimp.hpp" #include "types/types.hpp" @@ -77,20 +82,25 @@ namespace MWLua return &wm->getExterior(ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace)); } - void teleportPlayer( - MWWorld::CellStore* destCell, const osg::Vec3f& pos, const osg::Vec3f& rot, bool placeOnGround) + ESM::Position toPos(const osg::Vec3f& pos, const osg::Vec3f& rot) { - MWBase::World* world = MWBase::Environment::get().getWorld(); ESM::Position esmPos; static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2); std::memcpy(esmPos.pos, &pos, sizeof(osg::Vec3f)); std::memcpy(esmPos.rot, &rot, sizeof(osg::Vec3f)); + return esmPos; + } + + void teleportPlayer( + MWWorld::CellStore* destCell, const osg::Vec3f& pos, const osg::Vec3f& rot, bool placeOnGround) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptr = world->getPlayerPtr(); auto& stats = ptr.getClass().getCreatureStats(ptr); stats.land(true); stats.setTeleported(true); world->getPlayer().setTeleported(true); - world->changeToCell(destCell->getCell()->getId(), esmPos, false); + world->changeToCell(destCell->getCell()->getId(), toPos(pos, rot), false); MWWorld::Ptr newPtr = world->getPlayerPtr(); world->moveObject(newPtr, pos); world->rotateObject(newPtr, rot); @@ -103,15 +113,38 @@ namespace MWLua const osg::Vec3f& rot, bool placeOnGround) { MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::WorldModel* wm = MWBase::Environment::get().getWorldModel(); const MWWorld::Class& cls = ptr.getClass(); if (cls.isActor()) { - auto& stats = ptr.getClass().getCreatureStats(ptr); + auto& stats = cls.getCreatureStats(ptr); stats.land(false); stats.setTeleported(true); } - MWWorld::Ptr newPtr = world->moveObject(ptr, destCell, pos); - world->rotateObject(newPtr, rot, MWBase::RotationFlag_none); + const MWWorld::CellStore* srcCell = ptr.getCell(); + MWWorld::Ptr newPtr; + if (srcCell == &wm->getDraftCell()) + { + newPtr = cls.moveToCell(ptr, *destCell, toPos(pos, rot)); + ptr.getCellRef().unsetRefNum(); + ptr.getRefData().setLuaScripts(nullptr); + ptr.getCellRef().setCount(0); + ESM::RefId script = cls.getScript(newPtr); + if (!script.empty()) + world->getLocalScripts().add(script, newPtr); + world->addContainerScripts(newPtr, newPtr.getCell()); + } + else + { + newPtr = world->moveObject(ptr, destCell, pos); + if (srcCell == destCell) + { + ESM::RefId script = cls.getScript(newPtr); + if (!script.empty()) + world->getLocalScripts().add(script, newPtr); + } + world->rotateObject(newPtr, rot, MWBase::RotationFlag_none); + } if (placeOnGround) world->adjustPosition(newPtr, true); if (cls.isDoor()) @@ -131,16 +164,16 @@ namespace MWLua void registerObjectList(const std::string& prefix, const Context& context) { using ListT = ObjectList; - sol::state_view& lua = context.mLua->sol(); + sol::state_view lua = context.sol(); sol::usertype listT = lua.new_usertype(prefix + "ObjectList"); listT[sol::meta_function::to_string] = [](const ListT& list) { return "{" + std::to_string(list.mIds->size()) + " objects}"; }; listT[sol::meta_function::length] = [](const ListT& list) { return list.mIds->size(); }; - listT[sol::meta_function::index] = [](const ListT& list, size_t index) { + listT[sol::meta_function::index] = [](const ListT& list, size_t index) -> sol::optional { if (index > 0 && index <= list.mIds->size()) - return ObjectT((*list.mIds)[index - 1]); + return ObjectT((*list.mIds)[LuaUtil::fromLuaIndex(index)]); else - throw std::runtime_error("Index out of range"); + return sol::nullopt; }; listT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); listT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); @@ -168,6 +201,79 @@ namespace MWLua return Misc::Convert::makeOsgQuat(pos.rot); } + template + void addOwnerbindings(sol::usertype& objectT, const std::string& prefix, const Context& context) + { + using OwnerT = Owner; + sol::usertype ownerT = context.sol().new_usertype(prefix + "Owner"); + + ownerT[sol::meta_function::to_string] = [](const OwnerT& o) { return "Owner[" + o.mObj.toString() + "]"; }; + + auto getOwnerRecordId = [](const OwnerT& o) -> sol::optional { + ESM::RefId owner = o.mObj.ptr().getCellRef().getOwner(); + if (owner.empty()) + return sol::nullopt; + else + return owner.serializeText(); + }; + auto setOwnerRecordId = [](const OwnerT& o, sol::optional ownerId) { + if (std::is_same_v && !dynamic_cast(&o.mObj)) + throw std::runtime_error("Local scripts can set an owner only on self"); + const MWWorld::Ptr& ptr = o.mObj.ptr(); + + if (!ownerId) + { + ptr.getCellRef().setOwner(ESM::RefId()); + return; + } + ESM::RefId owner = ESM::RefId::deserializeText(*ownerId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + if (!store.get().search(owner)) + throw std::runtime_error("Invalid owner record id"); + ptr.getCellRef().setOwner(owner); + }; + ownerT["recordId"] = sol::property(getOwnerRecordId, setOwnerRecordId); + + auto getOwnerFactionId = [](const OwnerT& o) -> sol::optional { + ESM::RefId owner = o.mObj.ptr().getCellRef().getFaction(); + if (owner.empty()) + return sol::nullopt; + else + return owner.serializeText(); + }; + auto setOwnerFactionId = [](const OwnerT& o, sol::optional ownerId) { + ESM::RefId ownerFac; + if (std::is_same_v && !dynamic_cast(&o.mObj)) + throw std::runtime_error("Local scripts can set an owner faction only on self"); + if (!ownerId) + { + o.mObj.ptr().getCellRef().setFaction(ESM::RefId()); + return; + } + ownerFac = ESM::RefId::deserializeText(*ownerId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + if (!store.get().search(ownerFac)) + throw std::runtime_error("Invalid owner faction id"); + o.mObj.ptr().getCellRef().setFaction(ownerFac); + }; + ownerT["factionId"] = sol::property(getOwnerFactionId, setOwnerFactionId); + + auto getOwnerFactionRank = [](const OwnerT& o) -> sol::optional { + int rank = o.mObj.ptr().getCellRef().getFactionRank(); + if (rank < 0) + return sol::nullopt; + return LuaUtil::toLuaIndex(rank); + }; + auto setOwnerFactionRank = [](const OwnerT& o, sol::optional factionRank) { + if (std::is_same_v && !dynamic_cast(&o.mObj)) + throw std::runtime_error("Local scripts can set an owner faction rank only on self"); + o.mObj.ptr().getCellRef().setFactionRank(LuaUtil::fromLuaIndex(factionRank.value_or(0))); + }; + ownerT["factionRank"] = sol::property(getOwnerFactionRank, setOwnerFactionRank); + + objectT["owner"] = sol::readonly_property([](const ObjectT& object) { return OwnerT{ object }; }); + } + template void addBasicBindings(sol::usertype& objectT, const Context& context) { @@ -182,6 +288,13 @@ namespace MWLua objectT["isValid"] = [](const ObjectT& o) { return !o.ptrOrEmpty().isEmpty(); }; objectT["recordId"] = sol::readonly_property( [](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().serializeText(); }); + objectT["globalVariable"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { + std::string_view globalVariable = o.ptr().getCellRef().getGlobalVariable(); + if (globalVariable.empty()) + return sol::nullopt; + else + return ESM::RefId::stringRefId(globalVariable).serializeText(); + }); objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional> { const MWWorld::Ptr& ptr = o.ptr(); MWWorld::WorldModel* wm = MWBase::Environment::get().getWorldModel(); @@ -190,6 +303,13 @@ namespace MWLua else return sol::nullopt; }); + objectT["parentContainer"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { + const MWWorld::Ptr& ptr = o.ptr(); + if (ptr.getContainerStore()) + return ObjectT(ptr.getContainerStore()->getPtr()); + else + return sol::nullopt; + }); objectT["position"] = sol::readonly_property( [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asVec3(); }); objectT["scale"] @@ -209,73 +329,18 @@ namespace MWLua return LuaUtil::Box{ bb.center(), bb._max - bb.center() }; }; - objectT["type"] = sol::readonly_property( - [types = getTypeToPackageTable(context.mLua->sol())]( - const ObjectT& o) mutable { return types[getLiveCellRefType(o.ptr().mRef)]; }); + objectT["type"] + = sol::readonly_property([types = getTypeToPackageTable(context.sol())](const ObjectT& o) mutable { + return types[getLiveCellRefType(o.ptr().mRef)]; + }); - objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getRefData().getCount(); }); + objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getCellRef().getCount(); }); objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); }; objectT[sol::meta_function::to_string] = &ObjectT::toString; objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData) { context.mLuaEvents->addLocalEvent( { dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); }; - auto getOwnerRecordId = [](const ObjectT& o) -> sol::optional { - ESM::RefId owner = o.ptr().getCellRef().getOwner(); - if (owner.empty()) - return sol::nullopt; - else - return owner.serializeText(); - }; - auto setOwnerRecordId = [](const ObjectT& obj, sol::optional ownerId) { - if (std::is_same_v && !dynamic_cast(&obj)) - throw std::runtime_error("Local scripts can set an owner only on self"); - const MWWorld::Ptr& ptr = obj.ptr(); - - if (!ownerId) - { - ptr.getCellRef().setOwner(ESM::RefId()); - return; - } - ESM::RefId owner = ESM::RefId::deserializeText(*ownerId); - const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - if (!store.get().search(owner)) - throw std::runtime_error("Invalid owner record id"); - ptr.getCellRef().setOwner(owner); - }; - objectT["ownerRecordId"] = sol::property(getOwnerRecordId, setOwnerRecordId); - - auto getOwnerFactionId = [](const ObjectT& o) -> sol::optional { - ESM::RefId owner = o.ptr().getCellRef().getFaction(); - if (owner.empty()) - return sol::nullopt; - else - return owner.serializeText(); - }; - auto setOwnerFactionId = [](const ObjectT& object, sol::optional ownerId) { - ESM::RefId ownerFac; - if (std::is_same_v && !dynamic_cast(&object)) - throw std::runtime_error("Local scripts can set an owner faction only on self"); - if (!ownerId) - { - object.ptr().getCellRef().setFaction(ESM::RefId()); - return; - } - ownerFac = ESM::RefId::deserializeText(*ownerId); - const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - if (!store.get().search(ownerFac)) - throw std::runtime_error("Invalid owner faction id"); - object.ptr().getCellRef().setFaction(ownerFac); - }; - objectT["ownerFactionId"] = sol::property(getOwnerFactionId, setOwnerFactionId); - - auto getOwnerFactionRank = [](const ObjectT& o) -> int { return o.ptr().getCellRef().getFactionRank(); }; - auto setOwnerFactionRank = [](const ObjectT& object, int factionRank) { - if (std::is_same_v && !dynamic_cast(&object)) - throw std::runtime_error("Local scripts can set an owner faction rank only on self"); - object.ptr().getCellRef().setFactionRank(factionRank); - }; - objectT["ownerFactionRank"] = sol::property(getOwnerFactionRank, setOwnerFactionRank); objectT["activateBy"] = [](const ObjectT& object, const ObjectT& actor) { const MWWorld::Ptr& objPtr = object.ptr(); @@ -291,10 +356,10 @@ namespace MWLua auto isEnabled = [](const ObjectT& o) { return o.ptr().getRefData().isEnabled(); }; auto setEnabled = [context](const GObject& object, bool enable) { - if (enable && object.ptr().getRefData().isDeleted()) + if (enable && object.ptr().mRef->isDeleted()) throw std::runtime_error("Object is removed"); context.mLuaManager->addAction([object, enable] { - if (object.ptr().getRefData().isDeleted()) + if (object.ptr().mRef->isDeleted()) return; if (object.ptr().isInCell()) { @@ -325,7 +390,7 @@ namespace MWLua }; objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { const LuaUtil::ScriptsConfiguration& cfg = context.mLua->getConfiguration(); - std::optional scriptId = cfg.findId(path); + std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); if (!scriptId) throw std::runtime_error("Unknown script: " + std::string(path)); if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) @@ -342,7 +407,7 @@ namespace MWLua }; objectT["hasScript"] = [lua = context.mLua](const GObject& object, std::string_view path) { const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); - std::optional scriptId = cfg.findId(path); + std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); if (!scriptId) return false; MWWorld::Ptr ptr = object.ptr(); @@ -354,7 +419,7 @@ namespace MWLua }; objectT["removeScript"] = [lua = context.mLua](const GObject& object, std::string_view path) { const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); - std::optional scriptId = cfg.findId(path); + std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); if (!scriptId) throw std::runtime_error("Unknown script: " + std::string(path)); MWWorld::Ptr ptr = object.ptr(); @@ -368,20 +433,23 @@ namespace MWLua using DelayedRemovalFn = std::function; auto removeFn = [](const MWWorld::Ptr ptr, int countToRemove) -> std::optional { - int currentCount = ptr.getRefData().getCount(); + int rawCount = ptr.getCellRef().getCount(false); + int currentCount = std::abs(rawCount); + int signedCountToRemove = (rawCount < 0 ? -1 : 1) * countToRemove; + if (countToRemove <= 0 || countToRemove > currentCount) throw std::runtime_error("Can't remove " + std::to_string(countToRemove) + " of " + std::to_string(currentCount) + " items"); - ptr.getRefData().setCount(currentCount - countToRemove); // Immediately change count + ptr.getCellRef().setCount(rawCount - signedCountToRemove); // Immediately change count if (!ptr.getContainerStore() && currentCount > countToRemove) return std::nullopt; // Delayed action to trigger side effects - return [countToRemove](MWWorld::Ptr ptr) { + return [signedCountToRemove](MWWorld::Ptr ptr) { // Restore the original count - ptr.getRefData().setCount(ptr.getRefData().getCount() + countToRemove); + ptr.getCellRef().setCount(ptr.getCellRef().getCount(false) + signedCountToRemove); // And now remove properly if (ptr.getContainerStore()) - ptr.getContainerStore()->remove(ptr, countToRemove, false); + ptr.getContainerStore()->remove(ptr, std::abs(signedCountToRemove), false); else { MWBase::Environment::get().getWorld()->disable(ptr); @@ -391,7 +459,7 @@ namespace MWLua }; objectT["remove"] = [removeFn, context](const GObject& object, sol::optional count) { std::optional delayed - = removeFn(object.ptr(), count.value_or(object.ptr().getRefData().getCount())); + = removeFn(object.ptr(), count.value_or(object.ptr().getCellRef().getCount())); if (delayed.has_value()) context.mLuaManager->addAction([fn = *delayed, object] { fn(object.ptr()); }); }; @@ -411,7 +479,7 @@ namespace MWLua }; objectT["moveInto"] = [removeFn, context](const GObject& object, const sol::object& dest) { const MWWorld::Ptr& ptr = object.ptr(); - int count = ptr.getRefData().getCount(); + int count = ptr.getCellRef().getCount(); MWWorld::Ptr destPtr; if (dest.is()) destPtr = dest.as().ptr(); @@ -422,9 +490,9 @@ namespace MWLua std::optional delayedRemovalFn = removeFn(ptr, count); context.mLuaManager->addAction([item = object, count, cont = GObject(destPtr), delayedRemovalFn] { const MWWorld::Ptr& oldPtr = item.ptr(); - auto& refData = oldPtr.getRefData(); + auto& refData = oldPtr.getCellRef(); refData.setCount(count); // temporarily undo removal to run ContainerStore::add - refData.enable(); + oldPtr.getRefData().enable(); cont.ptr().getClass().getContainerStore(cont.ptr()).add(oldPtr, count, false); refData.setCount(0); if (delayedRemovalFn.has_value()) @@ -435,7 +503,7 @@ namespace MWLua const osg::Vec3f& pos, const sol::object& options) { MWWorld::CellStore* cell = findCell(cellOrName, pos); MWWorld::Ptr ptr = object.ptr(); - int count = ptr.getRefData().getCount(); + int count = ptr.getCellRef().getCount(); if (count == 0) throw std::runtime_error("Object is either removed or already in the process of teleporting"); osg::Vec3f rot = ptr.getRefData().getPosition().asRotationVec3(); @@ -456,9 +524,9 @@ namespace MWLua context.mLuaManager->addAction( [object, cell, pos, rot, count, delayedRemovalFn, placeOnGround] { MWWorld::Ptr oldPtr = object.ptr(); - oldPtr.getRefData().setCount(count); + oldPtr.getCellRef().setCount(count); MWWorld::Ptr newPtr = oldPtr.getClass().moveToCell(oldPtr, *cell); - oldPtr.getRefData().setCount(0); + oldPtr.getCellRef().setCount(0); newPtr.getRefData().disable(); teleportNotPlayer(newPtr, cell, pos, rot, placeOnGround); delayedRemovalFn(oldPtr); @@ -470,10 +538,10 @@ namespace MWLua [cell, pos, rot, placeOnGround] { teleportPlayer(cell, pos, rot, placeOnGround); }); else { - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); context.mLuaManager->addAction( [object, cell, pos, rot, count, placeOnGround] { - object.ptr().getRefData().setCount(count); + object.ptr().getCellRef().setCount(count); teleportNotPlayer(object.ptr(), cell, pos, rot, placeOnGround); }, "TeleportAction"); @@ -486,12 +554,12 @@ namespace MWLua void addInventoryBindings(sol::usertype& objectT, const std::string& prefix, const Context& context) { using InventoryT = Inventory; - sol::usertype inventoryT = context.mLua->sol().new_usertype(prefix + "Inventory"); + sol::usertype inventoryT = context.sol().new_usertype(prefix + "Inventory"); inventoryT[sol::meta_function::to_string] = [](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; }; - inventoryT["getAll"] = [ids = getPackageToTypeTable(context.mLua->sol())]( + inventoryT["getAll"] = [ids = getPackageToTypeTable(context.mLua->unsafeState())]( const InventoryT& inventory, sol::optional type) { int mask = -1; sol::optional typeId = sol::nullopt; @@ -558,13 +626,13 @@ namespace MWLua MWBase::Environment::get().getWorldModel()->registerPtr(item); list->push_back(getId(item)); } - return ObjectList{ list }; + return ObjectList{ std::move(list) }; }; inventoryT["countOf"] = [](const InventoryT& inventory, std::string_view recordId) { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - return store.count(ESM::RefId::stringRefId(recordId)); + return store.count(ESM::RefId::deserializeText(recordId)); }; if constexpr (std::is_same_v) { @@ -582,7 +650,7 @@ namespace MWLua inventoryT["find"] = [](const InventoryT& inventory, std::string_view recordId) -> sol::optional { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - auto itemId = ESM::RefId::stringRefId(recordId); + auto itemId = ESM::RefId::deserializeText(recordId); for (const MWWorld::Ptr& item : store) { if (item.getCellRef().getRefId() == itemId) @@ -596,7 +664,7 @@ namespace MWLua inventoryT["findAll"] = [](const InventoryT& inventory, std::string_view recordId) { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - auto itemId = ESM::RefId::stringRefId(recordId); + auto itemId = ESM::RefId::deserializeText(recordId); ObjectIdList list = std::make_shared>(); for (const MWWorld::Ptr& item : store) { @@ -606,7 +674,7 @@ namespace MWLua list->push_back(getId(item)); } } - return ObjectList{ list }; + return ObjectList{ std::move(list) }; }; } @@ -614,9 +682,10 @@ namespace MWLua void initObjectBindings(const std::string& prefix, const Context& context) { sol::usertype objectT - = context.mLua->sol().new_usertype(prefix + "Object", sol::base_classes, sol::bases()); + = context.sol().new_usertype(prefix + "Object", sol::base_classes, sol::bases()); addBasicBindings(objectT, context); addInventoryBindings(objectT, prefix, context); + addOwnerbindings(objectT, prefix, context); registerObjectList(prefix, context); } diff --git a/apps/openmw/mwlua/objectlists.cpp b/apps/openmw/mwlua/objectlists.cpp index 40b1ab93fa..d0bda5a644 100644 --- a/apps/openmw/mwlua/objectlists.cpp +++ b/apps/openmw/mwlua/objectlists.cpp @@ -7,7 +7,6 @@ #include #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwclass/container.hpp" @@ -75,7 +74,7 @@ namespace MWLua if (mChanged) { mList->clear(); - for (const ObjectId& id : mSet) + for (ObjectId id : mSet) mList->push_back(id); mChanged = false; } diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index de48064734..ea7baccb76 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -7,6 +7,7 @@ #include "../mwbase/luamanager.hpp" +#include "inputprocessor.hpp" #include "localscripts.hpp" namespace MWLua @@ -17,42 +18,14 @@ namespace MWLua public: PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + , mInputProcessor(this) { - registerEngineHandlers({ &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers, - &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mOnFrameHandlers, - &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved, &mQuestUpdate, &mUiModeChanged }); + registerEngineHandlers({ &mConsoleCommandHandlers, &mOnFrameHandlers, &mQuestUpdate, &mUiModeChanged }); } void processInputEvent(const MWBase::LuaManager::InputEvent& event) { - using InputEvent = MWBase::LuaManager::InputEvent; - switch (event.mType) - { - case InputEvent::KeyPressed: - callEngineHandlers(mKeyPressHandlers, std::get(event.mValue)); - break; - case InputEvent::KeyReleased: - callEngineHandlers(mKeyReleaseHandlers, std::get(event.mValue)); - break; - case InputEvent::ControllerPressed: - callEngineHandlers(mControllerButtonPressHandlers, std::get(event.mValue)); - break; - case InputEvent::ControllerReleased: - callEngineHandlers(mControllerButtonReleaseHandlers, std::get(event.mValue)); - break; - case InputEvent::Action: - callEngineHandlers(mActionHandlers, std::get(event.mValue)); - break; - case InputEvent::TouchPressed: - callEngineHandlers(mTouchpadPressed, std::get(event.mValue)); - break; - case InputEvent::TouchReleased: - callEngineHandlers(mTouchpadReleased, std::get(event.mValue)); - break; - case InputEvent::TouchMoved: - callEngineHandlers(mTouchpadMoved, std::get(event.mValue)); - break; - } + mInputProcessor.processInputEvent(event); } void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } @@ -66,25 +39,19 @@ namespace MWLua } // `arg` is either forwarded from MWGui::pushGuiMode or empty - void uiModeChanged(const MWWorld::Ptr& arg) + void uiModeChanged(ObjectId arg, bool byLuaAction) { - if (arg.isEmpty()) - callEngineHandlers(mUiModeChanged); + if (arg.isZeroOrUnset()) + callEngineHandlers(mUiModeChanged, byLuaAction); else - callEngineHandlers(mUiModeChanged, LObject(arg)); + callEngineHandlers(mUiModeChanged, byLuaAction, LObject(arg)); } private: + friend class MWLua::InputProcessor; + InputProcessor mInputProcessor; EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; - EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; - EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; - EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; - EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; - EngineHandlerList mActionHandlers{ "onInputAction" }; EngineHandlerList mOnFrameHandlers{ "onFrame" }; - EngineHandlerList mTouchpadPressed{ "onTouchPress" }; - EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; - EngineHandlerList mTouchpadMoved{ "onTouchMove" }; EngineHandlerList mQuestUpdate{ "onQuestUpdate" }; EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; }; diff --git a/apps/openmw/mwlua/postprocessingbindings.cpp b/apps/openmw/mwlua/postprocessingbindings.cpp index 5ce37d13da..e64bf0fa9e 100644 --- a/apps/openmw/mwlua/postprocessingbindings.cpp +++ b/apps/openmw/mwlua/postprocessingbindings.cpp @@ -1,5 +1,7 @@ #include "postprocessingbindings.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwrender/postprocessor.hpp" @@ -78,14 +80,14 @@ namespace MWLua for (size_t i = 0; i < *targetSize; ++i) { - sol::object obj = table[i + 1]; + sol::object obj = table[LuaUtil::toLuaIndex(i)]; if (!obj.is()) throw std::runtime_error("Invalid type for uniform array"); values.push_back(obj.as()); } context.mLuaManager->addAction( - [=] { + [shader, name, values = std::move(values)] { MWBase::Environment::get().getWorld()->getPostProcessor()->setUniform(shader.mShader, name, values); }, "SetUniformShaderAction"); @@ -94,9 +96,10 @@ namespace MWLua sol::table initPostprocessingPackage(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); - sol::usertype shader = context.mLua->sol().new_usertype("Shader"); + sol::usertype shader = lua.new_usertype("Shader"); shader[sol::meta_function::to_string] = [](const Shader& shader) { return shader.toString(); }; shader["enable"] = [context](Shader& shader, sol::optional optPos) { diff --git a/apps/openmw/mwlua/racebindings.cpp b/apps/openmw/mwlua/racebindings.cpp new file mode 100644 index 0000000000..b5c4d6093f --- /dev/null +++ b/apps/openmw/mwlua/racebindings.cpp @@ -0,0 +1,116 @@ +#include "racebindings.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwworld/esmstore.hpp" + +#include "idcollectionbindings.hpp" +#include "types/types.hpp" + +namespace +{ + struct RaceAttributes + { + const ESM::Race& mRace; + const sol::state_view mLua; + + sol::table getAttribute(ESM::RefId id) const + { + sol::table res(mLua, sol::create); + res["male"] = mRace.mData.getAttribute(id, true); + res["female"] = mRace.mData.getAttribute(id, false); + return LuaUtil::makeReadOnly(res); + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initRaceRecordBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table races(lua, sol::create); + addRecordFunctionBinding(races, context); + + auto raceT = lua.new_usertype("ESM3_Race"); + raceT[sol::meta_function::to_string] + = [](const ESM::Race& rec) -> std::string { return "ESM3_Race[" + rec.mId.toDebugString() + "]"; }; + raceT["id"] = sol::readonly_property([](const ESM::Race& rec) { return rec.mId.serializeText(); }); + raceT["name"] = sol::readonly_property([](const ESM::Race& rec) -> std::string_view { return rec.mName; }); + raceT["description"] + = sol::readonly_property([](const ESM::Race& rec) -> std::string_view { return rec.mDescription; }); + raceT["spells"] = sol::readonly_property( + [lua](const ESM::Race& rec) -> sol::table { return createReadOnlyRefIdTable(lua, rec.mPowers.mList); }); + raceT["skills"] = sol::readonly_property([lua](const ESM::Race& rec) -> sol::table { + sol::table res(lua, sol::create); + for (const auto& skillBonus : rec.mData.mBonus) + { + ESM::RefId skill = ESM::Skill::indexToRefId(skillBonus.mSkill); + if (!skill.empty()) + res[skill.serializeText()] = skillBonus.mBonus; + } + return res; + }); + raceT["isPlayable"] = sol::readonly_property( + [](const ESM::Race& rec) -> bool { return rec.mData.mFlags & ESM::Race::Playable; }); + raceT["isBeast"] + = sol::readonly_property([](const ESM::Race& rec) -> bool { return rec.mData.mFlags & ESM::Race::Beast; }); + raceT["height"] = sol::readonly_property([lua](const ESM::Race& rec) -> sol::table { + sol::table res(lua, sol::create); + res["male"] = rec.mData.mMaleHeight; + res["female"] = rec.mData.mFemaleHeight; + return LuaUtil::makeReadOnly(res); + }); + raceT["weight"] = sol::readonly_property([lua](const ESM::Race& rec) -> sol::table { + sol::table res(lua, sol::create); + res["male"] = rec.mData.mMaleWeight; + res["female"] = rec.mData.mFemaleWeight; + return LuaUtil::makeReadOnly(res); + }); + + raceT["attributes"] = sol::readonly_property([lua](const ESM::Race& rec) -> RaceAttributes { + return { rec, lua }; + }); + + auto attributesT = lua.new_usertype("ESM3_RaceAttributes"); + const auto& store = MWBase::Environment::get().getESMStore()->get(); + attributesT[sol::meta_function::index] + = [&](const RaceAttributes& attributes, std::string_view stringId) -> sol::optional { + ESM::RefId id = ESM::RefId::deserializeText(stringId); + if (!store.search(id)) + return sol::nullopt; + return attributes.getAttribute(id); + }; + attributesT[sol::meta_function::pairs] = [&](sol::this_state ts, RaceAttributes& attributes) { + auto iterator = store.begin(); + return sol::as_function( + [iterator, attributes, + &store]() mutable -> std::pair, sol::optional> { + if (iterator != store.end()) + { + ESM::RefId id = iterator->mId; + ++iterator; + return { id.serializeText(), attributes.getAttribute(id) }; + } + return { sol::nullopt, sol::nullopt }; + }); + }; + + return LuaUtil::makeReadOnly(races); + } +} diff --git a/apps/openmw/mwlua/racebindings.hpp b/apps/openmw/mwlua/racebindings.hpp new file mode 100644 index 0000000000..43ba9237c5 --- /dev/null +++ b/apps/openmw/mwlua/racebindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_RACEBINDINGS_H +#define MWLUA_RACEBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initRaceRecordBindings(const Context& context); +} + +#endif // MWLUA_RACEBINDINGS_H diff --git a/apps/openmw/mwlua/recordstore.hpp b/apps/openmw/mwlua/recordstore.hpp new file mode 100644 index 0000000000..aed84a1271 --- /dev/null +++ b/apps/openmw/mwlua/recordstore.hpp @@ -0,0 +1,64 @@ +#ifndef MWLUA_RECORDSTORE_H +#define MWLUA_RECORDSTORE_H + +#include + +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/store.hpp" + +#include "context.hpp" +#include "object.hpp" + +namespace sol +{ + // Ensure sol does not try to create the automatic Container or usertype bindings for Store. + // They include write operations and we want the store to be read-only. + template + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + template + void addRecordFunctionBinding( + sol::table& table, const Context& context, const std::string& recordName = std::string(T::getRecordType())) + { + const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); + + table["record"] = sol::overload([](const Object& obj) -> const T* { return obj.ptr().get()->mBase; }, + [&store](std::string_view id) -> const T* { return store.search(ESM::RefId::deserializeText(id)); }); + + // Define a custom user type for the store. + // Provide the interface of a read-only array. + using StoreT = MWWorld::Store; + sol::state_view lua = context.sol(); + sol::usertype storeT = lua.new_usertype(recordName + "WorldStore"); + storeT[sol::meta_function::to_string] = [recordName](const StoreT& store) { + return "{" + std::to_string(store.getSize()) + " " + recordName + " records}"; + }; + storeT[sol::meta_function::length] = [](const StoreT& store) { return store.getSize(); }; + storeT[sol::meta_function::index] = sol::overload( + [](const StoreT& store, size_t index) -> const T* { + if (index == 0 || index > store.getSize()) + return nullptr; + return store.at(LuaUtil::fromLuaIndex(index)); + }, + [](const StoreT& store, std::string_view id) -> const T* { + return store.search(ESM::RefId::deserializeText(id)); + }); + storeT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + storeT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + + // Provide access to the store. + table["records"] = &store; + } +} +#endif // MWLUA_RECORDSTORE_H diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index 355ede9f27..09309803d3 100644 --- a/apps/openmw/mwlua/soundbindings.cpp +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -1,4 +1,5 @@ #include "soundbindings.hpp" +#include "recordstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" @@ -11,6 +12,7 @@ #include #include "luamanagerimp.hpp" +#include "objectvariant.hpp" namespace { @@ -23,6 +25,32 @@ namespace float mTimeOffset = 0.f; }; + struct StreamMusicArgs + { + float mFade = 1.f; + }; + + MWWorld::Ptr getMutablePtrOrThrow(const MWLua::ObjectVariant& variant) + { + if (variant.isLObject()) + throw std::runtime_error("Local scripts can only modify object they are attached to."); + + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + MWWorld::Ptr getPtrOrThrow(const MWLua::ObjectVariant& variant) + { + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + PlaySoundArgs getPlaySoundArgs(const sol::optional& options) { PlaySoundArgs args; @@ -55,13 +83,28 @@ namespace return MWSound::PlayMode::NoEnvNoScaling; return MWSound::PlayMode::NoEnv; } + + StreamMusicArgs getStreamMusicArgs(const sol::optional& options) + { + StreamMusicArgs args; + + if (options.has_value()) + { + args.mFade = options->get_or("fadeOut", 1.f); + } + return args; + } } namespace MWLua { sol::table initAmbientPackage(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + sol::state_view lua = context.sol(); + if (lua["openmw_ambient"] != sol::nil) + return lua["openmw_ambient"]; + + sol::table api(lua, sol::create); api["playSound"] = [](std::string_view soundId, const sol::optional& options) { auto args = getPlaySoundArgs(options); @@ -95,88 +138,102 @@ namespace MWLua return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), fileName); }; - return LuaUtil::makeReadOnly(api); + api["streamMusic"] = [](std::string_view fileName, const sol::optional& options) { + auto args = getStreamMusicArgs(options); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->streamMusic(VFS::Path::Normalized(fileName), MWSound::MusicType::Normal, args.mFade); + }; + + api["say"] + = [luaManager = context.mLuaManager](std::string_view fileName, sol::optional text) { + MWBase::Environment::get().getSoundManager()->say(VFS::Path::Normalized(fileName)); + if (text) + luaManager->addUIMessage(*text); + }; + + api["stopSay"] = []() { MWBase::Environment::get().getSoundManager()->stopSay(MWWorld::ConstPtr()); }; + api["isSayActive"] + = []() { return MWBase::Environment::get().getSoundManager()->sayActive(MWWorld::ConstPtr()); }; + + api["isMusicPlaying"] = []() { return MWBase::Environment::get().getSoundManager()->isMusicPlaying(); }; + + api["stopMusic"] = []() { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (sndMgr->getMusicType() == MWSound::MusicType::MWScript) + return; + + sndMgr->stopMusic(); + }; + + lua["openmw_ambient"] = LuaUtil::makeReadOnly(api); + return lua["openmw_ambient"]; } sol::table initCoreSoundBindings(const Context& context) { - sol::state_view& lua = context.mLua->sol(); + sol::state_view lua = context.sol(); sol::table api(lua, sol::create); + api["isEnabled"] = []() { return MWBase::Environment::get().getSoundManager()->isEnabled(); }; + api["playSound3d"] - = [](std::string_view soundId, const Object& object, const sol::optional& options) { + = [](std::string_view soundId, const sol::object& object, const sol::optional& options) { auto args = getPlaySoundArgs(options); auto playMode = getPlayMode(args, true); ESM::RefId sound = ESM::RefId::deserializeText(soundId); + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); MWBase::Environment::get().getSoundManager()->playSound3D( - object.ptr(), sound, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + ptr, sound, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); }; api["playSoundFile3d"] - = [](std::string_view fileName, const Object& object, const sol::optional& options) { + = [](std::string_view fileName, const sol::object& object, const sol::optional& options) { auto args = getPlaySoundArgs(options); auto playMode = getPlayMode(args, true); + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); - MWBase::Environment::get().getSoundManager()->playSound3D(object.ptr(), fileName, args.mVolume, - args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, fileName, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); }; - api["stopSound3d"] = [](std::string_view soundId, const Object& object) { + api["stopSound3d"] = [](std::string_view soundId, const sol::object& object) { ESM::RefId sound = ESM::RefId::deserializeText(soundId); - MWBase::Environment::get().getSoundManager()->stopSound3D(object.ptr(), sound); + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->stopSound3D(ptr, sound); }; - api["stopSoundFile3d"] = [](std::string_view fileName, const Object& object) { - MWBase::Environment::get().getSoundManager()->stopSound3D(object.ptr(), fileName); + api["stopSoundFile3d"] = [](std::string_view fileName, const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->stopSound3D(ptr, fileName); }; - api["isSoundPlaying"] = [](std::string_view soundId, const Object& object) { + api["isSoundPlaying"] = [](std::string_view soundId, const sol::object& object) { ESM::RefId sound = ESM::RefId::deserializeText(soundId); - return MWBase::Environment::get().getSoundManager()->getSoundPlaying(object.ptr(), sound); + const MWWorld::Ptr& ptr = getPtrOrThrow(ObjectVariant(object)); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(ptr, sound); }; - api["isSoundFilePlaying"] = [](std::string_view fileName, const Object& object) { - return MWBase::Environment::get().getSoundManager()->getSoundPlaying(object.ptr(), fileName); + api["isSoundFilePlaying"] = [](std::string_view fileName, const sol::object& object) { + const MWWorld::Ptr& ptr = getPtrOrThrow(ObjectVariant(object)); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(ptr, fileName); }; - api["say"] = sol::overload( - [luaManager = context.mLuaManager]( - std::string_view fileName, const Object& object, sol::optional text) { - MWBase::Environment::get().getSoundManager()->say(object.ptr(), std::string(fileName)); - if (text) - luaManager->addUIMessage(*text); - }, - [luaManager = context.mLuaManager](std::string_view fileName, sol::optional text) { - MWBase::Environment::get().getSoundManager()->say(std::string(fileName)); - if (text) - luaManager->addUIMessage(*text); - }); - api["stopSay"] = sol::overload( - [](const Object& object) { - const MWWorld::Ptr& objPtr = object.ptr(); - MWBase::Environment::get().getSoundManager()->stopSay(objPtr); - }, - []() { MWBase::Environment::get().getSoundManager()->stopSay(MWWorld::ConstPtr()); }); - api["isSayActive"] = sol::overload( - [](const Object& object) { - const MWWorld::Ptr& objPtr = object.ptr(); - return MWBase::Environment::get().getSoundManager()->sayActive(objPtr); - }, - []() { return MWBase::Environment::get().getSoundManager()->sayActive(MWWorld::ConstPtr()); }); + api["say"] = [luaManager = context.mLuaManager]( + std::string_view fileName, const sol::object& object, sol::optional text) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->say(ptr, VFS::Path::Normalized(fileName)); + if (text) + luaManager->addUIMessage(*text); + }; + api["stopSay"] = [](const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->stopSay(ptr); + }; + api["isSayActive"] = [](const sol::object& object) { + const MWWorld::Ptr& ptr = getPtrOrThrow(ObjectVariant(object)); + return MWBase::Environment::get().getSoundManager()->sayActive(ptr); + }; - using SoundStore = MWWorld::Store; - sol::usertype soundStoreT = lua.new_usertype("ESM3_SoundStore"); - soundStoreT[sol::meta_function::to_string] - = [](const SoundStore& store) { return "ESM3_SoundStore{" + std::to_string(store.getSize()) + " sounds}"; }; - soundStoreT[sol::meta_function::length] = [](const SoundStore& store) { return store.getSize(); }; - soundStoreT[sol::meta_function::index] = sol::overload( - [](const SoundStore& store, size_t index) -> const ESM::Sound* { return store.at(index - 1); }, - [](const SoundStore& store, std::string_view soundId) -> const ESM::Sound* { - return store.find(ESM::RefId::deserializeText(soundId)); - }); - soundStoreT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); - soundStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); - - api["sounds"] = &MWBase::Environment::get().getWorld()->getStore().get(); + addRecordFunctionBinding(api, context); // Sound record auto soundT = lua.new_usertype("ESM3_Sound"); @@ -190,7 +247,7 @@ namespace MWLua soundT["maxRange"] = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMaxRange; }); soundT["fileName"] = sol::readonly_property([](const ESM::Sound& rec) -> std::string { - return VFS::Path::normalizeFilename(Misc::ResourceHelpers::correctSoundPath(rec.mSound)); + return Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(rec.mSound)).value(); }); return LuaUtil::makeReadOnly(api); diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp index c202e9dc33..317ffbd406 100644 --- a/apps/openmw/mwlua/stats.cpp +++ b/apps/openmw/mwlua/stats.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include "context.hpp" #include "localscripts.hpp" @@ -20,6 +22,7 @@ #include "../mwworld/esmstore.hpp" #include "objectvariant.hpp" +#include "recordstore.hpp" namespace { @@ -28,7 +31,7 @@ namespace using Index = const SelfObject::CachedStat::Index&; template - auto addIndexedAccessor(Index index) + auto addIndexedAccessor(auto index) { return [index](const sol::object& o) { return T::create(ObjectVariant(o), index); }; } @@ -51,7 +54,7 @@ namespace if (it != self->mStatsCache.end()) return it->second; } - return sol::make_object(context.mLua->sol(), getter(obj.ptr())); + return sol::make_object(context.mLua->unsafeState(), getter(obj.ptr())); } } @@ -70,6 +73,96 @@ namespace MWLua "StatUpdateAction"); } + static void setCreatureValue(Index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + if (prop == "current") + stats.setLevel(LuaUtil::cast(value)); + } + + static void setNpcValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getNpcStats(ptr); + if (prop == "progress") + stats.setLevelProgress(LuaUtil::cast(value)); + else if (prop == "skillIncreasesForAttribute") + stats.setSkillIncreasesForAttribute( + *std::get(index).getIf(), LuaUtil::cast(value)); + else if (prop == "skillIncreasesForSpecialization") + stats.setSkillIncreasesForSpecialization( + static_cast(std::get(index)), LuaUtil::cast(value)); + } + + class SkillIncreasesForAttributeStats + { + ObjectVariant mObject; + + public: + SkillIncreasesForAttributeStats(ObjectVariant object) + : mObject(std::move(object)) + { + } + + sol::object get(const Context& context, ESM::StringRefId attributeId) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, attributeId, "skillIncreasesForAttribute", + [attributeId](const MWWorld::Ptr& ptr) { + return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForAttribute(attributeId); + }); + } + + void set(const Context& context, ESM::StringRefId attributeId, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, attributeId, "skillIncreasesForAttribute" }] = value; + } + }; + + class SkillIncreasesForSpecializationStats + { + ObjectVariant mObject; + + public: + SkillIncreasesForSpecializationStats(ObjectVariant object) + : mObject(std::move(object)) + { + } + + sol::object get(const Context& context, int specialization) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, specialization, "skillIncreasesForSpecialization", + [specialization](const MWWorld::Ptr& ptr) { + return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForSpecialization( + static_cast(specialization)); + }); + } + + void set(const Context& context, int specialization, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, specialization, "skillIncreasesForSpecialization" }] + = value; + } + }; + class LevelStat { ObjectVariant mObject; @@ -82,7 +175,7 @@ namespace MWLua public: sol::object getCurrent(const Context& context) const { - return getValue(context, mObject, &LevelStat::setValue, 0, "current", + return getValue(context, mObject, &setCreatureValue, std::monostate{}, "current", [](const MWWorld::Ptr& ptr) { return ptr.getClass().getCreatureStats(ptr).getLevel(); }); } @@ -90,7 +183,7 @@ namespace MWLua { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); - obj->mStatsCache[SelfObject::CachedStat{ &LevelStat::setValue, 0, "current" }] = value; + obj->mStatsCache[SelfObject::CachedStat{ &setCreatureValue, std::monostate{}, "current" }] = value; } sol::object getProgress(const Context& context) const @@ -98,7 +191,30 @@ namespace MWLua const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return sol::nil; - return sol::make_object(context.mLua->sol(), ptr.getClass().getNpcStats(ptr).getLevelProgress()); + + return getValue(context, mObject, &setNpcValue, std::monostate{}, "progress", + [](const MWWorld::Ptr& ptr) { return ptr.getClass().getNpcStats(ptr).getLevelProgress(); }); + } + + void setProgress(const Context& context, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, std::monostate{}, "progress" }] = value; + } + + SkillIncreasesForAttributeStats getSkillIncreasesForAttributeStats() const + { + return SkillIncreasesForAttributeStats{ mObject }; + } + + SkillIncreasesForSpecializationStats getSkillIncreasesForSpecializationStats() const + { + return SkillIncreasesForSpecializationStats{ mObject }; } static std::optional create(ObjectVariant object, Index) @@ -107,13 +223,6 @@ namespace MWLua return {}; return LevelStat{ std::move(object) }; } - - static void setValue(Index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) - { - auto& stats = ptr.getClass().getCreatureStats(ptr); - if (prop == "current") - stats.setLevel(LuaUtil::cast(value)); - } }; class DynamicStat @@ -316,10 +425,74 @@ namespace MWLua stats.setSkill(id, stat); } }; + + class AIStat + { + ObjectVariant mObject; + MWMechanics::AiSetting mIndex; + + AIStat(ObjectVariant object, MWMechanics::AiSetting index) + : mObject(std::move(object)) + , mIndex(index) + { + } + + public: + template + sol::object get(const Context& context, std::string_view prop, G getter) const + { + return getValue(context, mObject, &AIStat::setValue, static_cast(mIndex), prop, + [this, getter](const MWWorld::Ptr& ptr) { + return (ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex).*getter)(); + }); + } + + int getModified(const Context& context) const + { + auto base = LuaUtil::cast(get(context, "base", &MWMechanics::Stat::getBase)); + auto modifier = LuaUtil::cast(get(context, "modifier", &MWMechanics::Stat::getModifier)); + return std::max(0, base + modifier); + } + + static std::optional create(ObjectVariant object, MWMechanics::AiSetting index) + { + if (!object.ptr().getClass().isActor()) + return {}; + return AIStat{ std::move(object), index }; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &AIStat::setValue, static_cast(mIndex), prop }] = value; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto index = static_cast(std::get(i)); + auto& stats = ptr.getClass().getCreatureStats(ptr); + auto stat = stats.getAiSetting(index); + int intValue = LuaUtil::cast(value); + if (prop == "base") + stat.setBase(intValue); + else if (prop == "modifier") + stat.setModifier(intValue); + stats.setAiSetting(index, stat); + } + }; } namespace sol { + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; template <> struct is_automagical : std::false_type { @@ -336,61 +509,197 @@ namespace sol struct is_automagical : std::false_type { }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua { void addActorStatsBindings(sol::table& actor, const Context& context) { - sol::table stats(context.mLua->sol(), sol::create); + sol::state_view lua = context.sol(); + sol::table stats(lua, sol::create); actor["stats"] = LuaUtil::makeReadOnly(stats); - auto levelStatT = context.mLua->sol().new_usertype("LevelStat"); + auto skillIncreasesForAttributeStatsT + = lua.new_usertype("SkillIncreasesForAttributeStats"); + for (const auto& attribute : MWBase::Environment::get().getESMStore()->get()) + { + skillIncreasesForAttributeStatsT[ESM::RefId(attribute.mId).serializeText()] = sol::property( + [=](const SkillIncreasesForAttributeStats& stat) { return stat.get(context, attribute.mId); }, + [=](const SkillIncreasesForAttributeStats& stat, const sol::object& value) { + stat.set(context, attribute.mId, value); + }); + } + // ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization) + auto skillIncreasesForSpecializationStatsT + = lua.new_usertype("skillIncreasesForSpecializationStats"); + for (int i = 0; i < 3; i++) + { + std::string_view index = ESM::Class::specializationIndexToLuaId.at(i); + skillIncreasesForSpecializationStatsT[index] + = sol::property([=](const SkillIncreasesForSpecializationStats& stat) { return stat.get(context, i); }, + [=](const SkillIncreasesForSpecializationStats& stat, const sol::object& value) { + stat.set(context, i, value); + }); + } + + auto levelStatT = lua.new_usertype("LevelStat"); levelStatT["current"] = sol::property([context](const LevelStat& stat) { return stat.getCurrent(context); }, [context](const LevelStat& stat, const sol::object& value) { stat.setCurrent(context, value); }); - levelStatT["progress"] = sol::property([context](const LevelStat& stat) { return stat.getProgress(context); }); + levelStatT["progress"] = sol::property([context](const LevelStat& stat) { return stat.getProgress(context); }, + [context](const LevelStat& stat, const sol::object& value) { stat.setProgress(context, value); }); + levelStatT["skillIncreasesForAttribute"] + = sol::readonly_property([](const LevelStat& stat) { return stat.getSkillIncreasesForAttributeStats(); }); + levelStatT["skillIncreasesForSpecialization"] = sol::readonly_property( + [](const LevelStat& stat) { return stat.getSkillIncreasesForSpecializationStats(); }); stats["level"] = addIndexedAccessor(0); - auto dynamicStatT = context.mLua->sol().new_usertype("DynamicStat"); + auto dynamicStatT = lua.new_usertype("DynamicStat"); addProp(context, dynamicStatT, "base", &MWMechanics::DynamicStat::getBase); addProp(context, dynamicStatT, "current", &MWMechanics::DynamicStat::getCurrent); addProp(context, dynamicStatT, "modifier", &MWMechanics::DynamicStat::getModifier); - sol::table dynamic(context.mLua->sol(), sol::create); + sol::table dynamic(lua, sol::create); stats["dynamic"] = LuaUtil::makeReadOnly(dynamic); dynamic["health"] = addIndexedAccessor(0); dynamic["magicka"] = addIndexedAccessor(1); dynamic["fatigue"] = addIndexedAccessor(2); - auto attributeStatT = context.mLua->sol().new_usertype("AttributeStat"); + auto attributeStatT = lua.new_usertype("AttributeStat"); addProp(context, attributeStatT, "base", &MWMechanics::AttributeValue::getBase); addProp(context, attributeStatT, "damage", &MWMechanics::AttributeValue::getDamage); attributeStatT["modified"] - = sol::property([=](const AttributeStat& stat) { return stat.getModified(context); }); + = sol::readonly_property([=](const AttributeStat& stat) { return stat.getModified(context); }); addProp(context, attributeStatT, "modifier", &MWMechanics::AttributeValue::getModifier); - sol::table attributes(context.mLua->sol(), sol::create); + sol::table attributes(lua, sol::create); stats["attributes"] = LuaUtil::makeReadOnly(attributes); for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) attributes[ESM::RefId(attribute.mId).serializeText()] = addIndexedAccessor(attribute.mId); + + auto aiStatT = lua.new_usertype("AIStat"); + addProp(context, aiStatT, "base", &MWMechanics::Stat::getBase); + addProp(context, aiStatT, "modifier", &MWMechanics::Stat::getModifier); + aiStatT["modified"] = sol::readonly_property([=](const AIStat& stat) { return stat.getModified(context); }); + sol::table ai(lua, sol::create); + stats["ai"] = LuaUtil::makeReadOnly(ai); + ai["alarm"] = addIndexedAccessor(MWMechanics::AiSetting::Alarm); + ai["fight"] = addIndexedAccessor(MWMechanics::AiSetting::Fight); + ai["flee"] = addIndexedAccessor(MWMechanics::AiSetting::Flee); + ai["hello"] = addIndexedAccessor(MWMechanics::AiSetting::Hello); } void addNpcStatsBindings(sol::table& npc, const Context& context) { - sol::table npcStats(context.mLua->sol(), sol::create); - sol::table baseMeta(context.mLua->sol(), sol::create); + sol::state_view lua = context.sol(); + sol::table npcStats(lua, sol::create); + sol::table baseMeta(lua, sol::create); baseMeta[sol::meta_function::index] = LuaUtil::getMutableFromReadOnly(npc["baseType"]["stats"]); npcStats[sol::metatable_key] = baseMeta; npc["stats"] = LuaUtil::makeReadOnly(npcStats); - auto skillStatT = context.mLua->sol().new_usertype("SkillStat"); + auto skillStatT = lua.new_usertype("SkillStat"); addProp(context, skillStatT, "base", &MWMechanics::SkillValue::getBase); addProp(context, skillStatT, "damage", &MWMechanics::SkillValue::getDamage); - skillStatT["modified"] = sol::property([=](const SkillStat& stat) { return stat.getModified(context); }); + skillStatT["modified"] + = sol::readonly_property([=](const SkillStat& stat) { return stat.getModified(context); }); addProp(context, skillStatT, "modifier", &MWMechanics::SkillValue::getModifier); skillStatT["progress"] = sol::property([context](const SkillStat& stat) { return stat.getProgress(context); }, [context](const SkillStat& stat, const sol::object& value) { stat.cache(context, "progress", value); }); - sol::table skills(context.mLua->sol(), sol::create); + sol::table skills(lua, sol::create); npcStats["skills"] = LuaUtil::makeReadOnly(skills); for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) skills[ESM::RefId(skill.mId).serializeText()] = addIndexedAccessor(skill.mId); } + + sol::table initCoreStatsBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table statsApi(lua, sol::create); + auto* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + sol::table attributes(lua, sol::create); + addRecordFunctionBinding(attributes, context); + statsApi["Attribute"] = LuaUtil::makeReadOnly(attributes); + statsApi["Attribute"][sol::metatable_key][sol::meta_function::to_string] = ESM::Attribute::getRecordType; + + auto attributeT = lua.new_usertype("Attribute"); + attributeT[sol::meta_function::to_string] + = [](const ESM::Attribute& rec) { return "ESM3_Attribute[" + rec.mId.toDebugString() + "]"; }; + attributeT["id"] = sol::readonly_property( + [](const ESM::Attribute& rec) -> std::string { return ESM::RefId{ rec.mId }.serializeText(); }); + attributeT["name"] + = sol::readonly_property([](const ESM::Attribute& rec) -> std::string_view { return rec.mName; }); + attributeT["description"] + = sol::readonly_property([](const ESM::Attribute& rec) -> std::string_view { return rec.mDescription; }); + attributeT["icon"] = sol::readonly_property([vfs](const ESM::Attribute& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + + sol::table skills(lua, sol::create); + addRecordFunctionBinding(skills, context); + statsApi["Skill"] = LuaUtil::makeReadOnly(skills); + statsApi["Skill"][sol::metatable_key][sol::meta_function::to_string] = ESM::Skill::getRecordType; + + auto skillT = lua.new_usertype("Skill"); + skillT[sol::meta_function::to_string] + = [](const ESM::Skill& rec) { return "ESM3_Skill[" + rec.mId.toDebugString() + "]"; }; + skillT["id"] = sol::readonly_property( + [](const ESM::Skill& rec) -> std::string { return ESM::RefId{ rec.mId }.serializeText(); }); + skillT["name"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mName; }); + skillT["description"] + = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mDescription; }); + skillT["specialization"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { + return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); + }); + skillT["icon"] = sol::readonly_property([vfs](const ESM::Skill& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + skillT["school"] = sol::readonly_property([](const ESM::Skill& rec) -> const ESM::MagicSchool* { + if (!rec.mSchool) + return nullptr; + return &*rec.mSchool; + }); + skillT["attribute"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string { + return ESM::Attribute::indexToRefId(rec.mData.mAttribute).serializeText(); + }); + skillT["skillGain"] = sol::readonly_property([lua](const ESM::Skill& rec) -> sol::table { + sol::table res(lua, sol::create); + int index = 1; + for (auto skillGain : rec.mData.mUseValue) + res[index++] = skillGain; + return res; + }); + + auto schoolT = lua.new_usertype("MagicSchool"); + schoolT[sol::meta_function::to_string] + = [](const ESM::MagicSchool& rec) { return "ESM3_MagicSchool[" + rec.mName + "]"; }; + schoolT["name"] + = sol::readonly_property([](const ESM::MagicSchool& rec) -> std::string_view { return rec.mName; }); + schoolT["areaSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mAreaSound.serializeText(); }); + schoolT["boltSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mBoltSound.serializeText(); }); + schoolT["castSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mCastSound.serializeText(); }); + schoolT["failureSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mFailureSound.serializeText(); }); + schoolT["hitSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mHitSound.serializeText(); }); + + return LuaUtil::makeReadOnly(statsApi); + } } diff --git a/apps/openmw/mwlua/stats.hpp b/apps/openmw/mwlua/stats.hpp index 8c5824cc71..4ce2f6b5eb 100644 --- a/apps/openmw/mwlua/stats.hpp +++ b/apps/openmw/mwlua/stats.hpp @@ -9,6 +9,7 @@ namespace MWLua void addActorStatsBindings(sol::table& actor, const Context& context); void addNpcStatsBindings(sol::table& npc, const Context& context); + sol::table initCoreStatsBindings(const Context& context); } #endif diff --git a/apps/openmw/mwlua/types/activator.cpp b/apps/openmw/mwlua/types/activator.cpp index 9fec22b221..a366256899 100644 --- a/apps/openmw/mwlua/types/activator.cpp +++ b/apps/openmw/mwlua/types/activator.cpp @@ -1,14 +1,13 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include - namespace sol { template <> @@ -22,11 +21,19 @@ namespace ESM::Activator tableToActivator(const sol::table& rec) { ESM::Activator activator; - activator.mName = rec["name"]; - activator.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - std::string_view scriptId = rec["mwscript"].get(); - activator.mScript = ESM::RefId::deserializeText(scriptId); - activator.mRecordFlags = 0; + if (rec["template"] != sol::nil) + activator = LuaUtil::cast(rec["template"]); + else + activator.blank(); + if (rec["name"] != sol::nil) + activator.mName = rec["name"]; + if (rec["model"] != sol::nil) + activator.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + activator.mScript = ESM::RefId::deserializeText(scriptId); + } return activator; } } @@ -35,21 +42,18 @@ namespace MWLua { void addActivatorBindings(sol::table activator, const Context& context) { - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - activator["createRecordDraft"] = tableToActivator; addRecordFunctionBinding(activator, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Activator"); + sol::usertype record = context.sol().new_usertype("ESM3_Activator"); record[sol::meta_function::to_string] = [](const ESM::Activator& rec) { return "ESM3_Activator[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Activator& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Activator& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Activator& rec) -> std::string { return rec.mScript.serializeText(); }); } } diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp index 08fd98c41c..02686684e8 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -4,9 +4,13 @@ #include #include +#include +#include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/mechanicsmanager.hpp" #include "apps/openmw/mwbase/windowmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/actorutil.hpp" #include "apps/openmw/mwmechanics/creaturestats.hpp" #include "apps/openmw/mwmechanics/drawstate.hpp" #include "apps/openmw/mwworld/class.hpp" @@ -35,7 +39,7 @@ namespace MWLua itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(std::get(item)); if (old_it != store.end() && *old_it == itemPtr) return { old_it, true }; // already equipped - if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 + if (itemPtr.isEmpty() || itemPtr.getCellRef().getCount() == 0 || itemPtr.getContainerStore() != static_cast(&store)) { Log(Debug::Warning) << "Object" << std::get(item).toString() << " is not in inventory"; @@ -49,7 +53,7 @@ namespace MWLua if (old_it != store.end() && old_it->getCellRef().getRefId() == recordId) return { old_it, true }; // already equipped itemPtr = store.search(recordId); - if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) + if (itemPtr.isEmpty() || itemPtr.getCellRef().getCount() == 0) { Log(Debug::Warning) << "There is no object with recordId='" << stringId << "' in inventory"; return { store.end(), false }; @@ -66,11 +70,12 @@ namespace MWLua static void setEquipment(const MWWorld::Ptr& actor, const Equipment& equipment) { + bool isPlayer = actor == MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); std::array usedSlots; std::fill(usedSlots.begin(), usedSlots.end(), false); - auto tryEquipToSlot = [&store, &usedSlots](int slot, const EquipmentItem& item) -> bool { + auto tryEquipToSlot = [&store, &usedSlots, isPlayer](int slot, const EquipmentItem& item) -> bool { auto [it, alreadyEquipped] = findInInventory(store, item, slot); if (alreadyEquipped) return true; @@ -93,7 +98,22 @@ namespace MWLua slot = *firstAllowed; } - store.equip(slot, it); + bool skipEquip = false; + + if (isPlayer) + { + const ESM::RefId& script = itemPtr.getClass().getScript(itemPtr); + if (!script.empty()) + { + MWScript::Locals& locals = itemPtr.getRefData().getLocals(); + locals.setVarByInt(script, "onpcequip", 1); + skipEquip = locals.getIntVar(script, "pcskipequip") == 1; + } + } + + if (!skipEquip) + store.equip(slot, it); + return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed }; @@ -152,13 +172,15 @@ namespace MWLua void addActorBindings(sol::table actor, const Context& context) { + sol::state_view lua = context.sol(); actor["STANCE"] - = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Nothing", MWMechanics::DrawState::Nothing }, - { "Weapon", MWMechanics::DrawState::Weapon }, - { "Spell", MWMechanics::DrawState::Spell }, - })); - actor["EQUIPMENT_SLOT"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs( + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Nothing", MWMechanics::DrawState::Nothing }, + { "Weapon", MWMechanics::DrawState::Weapon }, + { "Spell", MWMechanics::DrawState::Spell }, + })); + actor["EQUIPMENT_SLOT"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, { { "Helmet", MWWorld::InventoryStore::Slot_Helmet }, { "Cuirass", MWWorld::InventoryStore::Slot_Cuirass }, { "Greaves", MWWorld::InventoryStore::Slot_Greaves }, { "LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron }, @@ -254,7 +276,8 @@ namespace MWLua { ei = LuaUtil::cast(item); } - context.mLuaManager->addAction([obj = Object(ptr), ei = ei] { setSelectedEnchantedItem(obj.ptr(), ei); }, + context.mLuaManager->addAction( + [obj = Object(ptr), ei = std::move(ei)] { setSelectedEnchantedItem(obj.ptr(), ei); }, "setSelectedEnchantedItemAction"); }; @@ -358,6 +381,39 @@ namespace MWLua result["halfExtents"] = agentBounds.mHalfExtents; return result; }; + actor["isInActorsProcessingRange"] = [](const Object& o) { + const MWWorld::Ptr player = MWMechanics::getPlayer(); + const auto& target = o.ptr(); + if (target == player) + return true; + + if (!target.getClass().isActor()) + throw std::runtime_error("Actor expected"); + + if (target.getCell()->getCell()->getWorldSpace() != player.getCell()->getCell()->getWorldSpace()) + return false; + + const int actorsProcessingRange = Settings::game().mActorsProcessingRange; + const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); + + const float dist = (playerPos - target.getRefData().getPosition().asVec3()).length(); + return dist <= actorsProcessingRange; + }; + + actor["isDead"] = [](const Object& o) { + const auto& target = o.ptr(); + return target.getClass().getCreatureStats(target).isDead(); + }; + + actor["isDeathFinished"] = [](const Object& o) { + const auto& target = o.ptr(); + return target.getClass().getCreatureStats(target).isDeathAnimationFinished(); + }; + + actor["getEncumbrance"] = [](const Object& actor) -> float { + const MWWorld::Ptr ptr = actor.ptr(); + return ptr.getClass().getEncumbrance(ptr); + }; addActorStatsBindings(actor, context); addActorMagicBindings(actor, context); diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp new file mode 100644 index 0000000000..425e44451b --- /dev/null +++ b/apps/openmw/mwlua/types/actor.hpp @@ -0,0 +1,84 @@ +#ifndef MWLUA_ACTOR_H +#define MWLUA_ACTOR_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" + +#include "../context.hpp" + +namespace MWLua +{ + + template + void addActorServicesBindings(sol::usertype& record, const Context& context) + { + record["servicesOffered"] = sol::readonly_property([context](const T& rec) -> sol::table { + sol::state_view lua = context.sol(); + sol::table providedServices(lua, sol::create); + constexpr std::array, 19> serviceNames = { { { ESM::NPC::Spells, + "Spells" }, + { ESM::NPC::Spellmaking, "Spellmaking" }, { ESM::NPC::Enchanting, "Enchanting" }, + { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, { ESM::NPC::AllItems, "Barter" }, + { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, { ESM::NPC::Clothing, "Clothing" }, + { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, { ESM::NPC::Picks, "Picks" }, + { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, { ESM::NPC::Apparatus, "Apparatus" }, + { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, { ESM::NPC::Potions, "Potions" }, + { ESM::NPC::MagicItems, "MagicItems" } } }; + + int services = rec.mAiData.mServices; + if constexpr (std::is_same_v) + { + if (rec.mFlags & ESM::NPC::Autocalc) + services + = MWBase::Environment::get().getESMStore()->get().find(rec.mClass)->mData.mServices; + } + for (const auto& [flag, name] : serviceNames) + { + providedServices[name] = (services & flag) != 0; + } + providedServices["Travel"] = !rec.getTransport().empty(); + return LuaUtil::makeReadOnly(providedServices); + }); + + record["travelDestinations"] = sol::readonly_property([context](const T& rec) -> sol::table { + sol::state_view lua = context.sol(); + sol::table travelDests(lua, sol::create); + if (!rec.getTransport().empty()) + { + int index = 1; + for (const auto& dest : rec.getTransport()) + { + sol::table travelDest(lua, sol::create); + + ESM::RefId cellId; + if (dest.mCellName.empty()) + { + const ESM::ExteriorCellLocation cellIndex + = ESM::positionToExteriorCellLocation(dest.mPos.pos[0], dest.mPos.pos[1]); + cellId = ESM::RefId::esm3ExteriorCell(cellIndex.mX, cellIndex.mY); + } + else + cellId = ESM::RefId::stringRefId(dest.mCellName); + travelDest["rotation"] = LuaUtil::asTransform(Misc::Convert::makeOsgQuat(dest.mPos.rot)); + travelDest["position"] = dest.mPos.asVec3(); + travelDest["cellId"] = cellId.serializeText(); + + travelDests[index] = LuaUtil::makeReadOnly(travelDest); + index++; + } + } + return LuaUtil::makeReadOnly(travelDests); + }); + } +} +#endif // MWLUA_ACTOR_H diff --git a/apps/openmw/mwlua/types/apparatus.cpp b/apps/openmw/mwlua/types/apparatus.cpp index 10bdbcdd29..c9953d87ad 100644 --- a/apps/openmw/mwlua/types/apparatus.cpp +++ b/apps/openmw/mwlua/types/apparatus.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -21,28 +22,29 @@ namespace MWLua { void addApparatusBindings(sol::table apparatus, const Context& context) { - apparatus["TYPE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "MortarPestle", ESM::Apparatus::MortarPestle }, - { "Alembic", ESM::Apparatus::Alembic }, - { "Calcinator", ESM::Apparatus::Calcinator }, - { "Retort", ESM::Apparatus::Retort }, - })); + sol::state_view lua = context.sol(); + apparatus["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "MortarPestle", ESM::Apparatus::MortarPestle }, + { "Alembic", ESM::Apparatus::Alembic }, + { "Calcinator", ESM::Apparatus::Calcinator }, + { "Retort", ESM::Apparatus::Retort }, + })); auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addRecordFunctionBinding(apparatus, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Apparatus"); + sol::usertype record = lua.new_usertype("ESM3_Apparatus"); record[sol::meta_function::to_string] = [](const ESM::Apparatus& rec) { return "ESM3_Apparatus[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Apparatus& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Apparatus& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Apparatus& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Apparatus& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/armor.cpp b/apps/openmw/mwlua/types/armor.cpp index 9f28f56e2f..0acb517dbe 100644 --- a/apps/openmw/mwlua/types/armor.cpp +++ b/apps/openmw/mwlua/types/armor.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -22,25 +23,45 @@ namespace ESM::Armor tableToArmor(const sol::table& rec) { ESM::Armor armor; - armor.mName = rec["name"]; - armor.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - armor.mIcon = rec["icon"]; - std::string_view enchantId = rec["enchant"].get(); - armor.mEnchant = ESM::RefId::deserializeText(enchantId); - std::string_view scriptId = rec["mwscript"].get(); - armor.mScript = ESM::RefId::deserializeText(scriptId); - - armor.mData.mWeight = rec["weight"]; - armor.mData.mValue = rec["value"]; - int armorType = rec["type"].get(); - if (armorType >= 0 && armorType <= ESM::Armor::RBracer) - armor.mData.mType = armorType; + if (rec["template"] != sol::nil) + armor = LuaUtil::cast(rec["template"]); else - throw std::runtime_error("Invalid Armor Type provided: " + std::to_string(armorType)); - armor.mData.mHealth = rec["health"]; - armor.mData.mArmor = rec["baseArmor"]; - armor.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); - armor.mRecordFlags = 0; + armor.blank(); + if (rec["name"] != sol::nil) + armor.mName = rec["name"]; + if (rec["model"] != sol::nil) + armor.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + armor.mIcon = rec["icon"]; + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + armor.mEnchant = ESM::RefId::deserializeText(enchantId); + } + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + armor.mScript = ESM::RefId::deserializeText(scriptId); + } + + if (rec["weight"] != sol::nil) + armor.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + armor.mData.mValue = rec["value"]; + if (rec["type"] != sol::nil) + { + int armorType = rec["type"].get(); + if (armorType >= 0 && armorType <= ESM::Armor::RBracer) + armor.mData.mType = armorType; + else + throw std::runtime_error("Invalid Armor Type provided: " + std::to_string(armorType)); + } + if (rec["health"] != sol::nil) + armor.mData.mHealth = rec["health"]; + if (rec["baseArmor"] != sol::nil) + armor.mData.mArmor = rec["baseArmor"]; + if (rec["enchantCapacity"] != sol::nil) + armor.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); return armor; } @@ -50,41 +71,41 @@ namespace MWLua { void addArmorBindings(sol::table armor, const Context& context) { - armor["TYPE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Helmet", ESM::Armor::Helmet }, - { "Cuirass", ESM::Armor::Cuirass }, - { "LPauldron", ESM::Armor::LPauldron }, - { "RPauldron", ESM::Armor::RPauldron }, - { "Greaves", ESM::Armor::Greaves }, - { "Boots", ESM::Armor::Boots }, - { "LGauntlet", ESM::Armor::LGauntlet }, - { "RGauntlet", ESM::Armor::RGauntlet }, - { "Shield", ESM::Armor::Shield }, - { "LBracer", ESM::Armor::LBracer }, - { "RBracer", ESM::Armor::RBracer }, - })); + sol::state_view lua = context.sol(); + armor["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Helmet", ESM::Armor::Helmet }, + { "Cuirass", ESM::Armor::Cuirass }, + { "LPauldron", ESM::Armor::LPauldron }, + { "RPauldron", ESM::Armor::RPauldron }, + { "Greaves", ESM::Armor::Greaves }, + { "Boots", ESM::Armor::Boots }, + { "LGauntlet", ESM::Armor::LGauntlet }, + { "RGauntlet", ESM::Armor::RGauntlet }, + { "Shield", ESM::Armor::Shield }, + { "LBracer", ESM::Armor::LBracer }, + { "RBracer", ESM::Armor::RBracer }, + })); auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addRecordFunctionBinding(armor, context); armor["createRecordDraft"] = tableToArmor; - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Armor"); + sol::usertype record = lua.new_usertype("ESM3_Armor"); record[sol::meta_function::to_string] = [](const ESM::Armor& rec) -> std::string { return "ESM3_Armor[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Armor& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); record["icon"] = sol::readonly_property([vfs](const ESM::Armor& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); - record["enchant"] - = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mEnchant.serializeText(); }); - record["mwscript"] - = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mScript.serializeText(); }); + record["enchant"] = sol::readonly_property( + [](const ESM::Armor& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mEnchant); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Armor& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["weight"] = sol::readonly_property([](const ESM::Armor& rec) -> float { return rec.mData.mWeight; }); record["value"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mValue; }); record["type"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mType; }); diff --git a/apps/openmw/mwlua/types/book.cpp b/apps/openmw/mwlua/types/book.cpp index 9501691a72..b133a90c3a 100644 --- a/apps/openmw/mwlua/types/book.cpp +++ b/apps/openmw/mwlua/types/book.cpp @@ -1,17 +1,17 @@ #include "types.hpp" -#include -#include +#include "modelproperty.hpp" #include +#include #include +#include #include +#include +#include #include -#include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -27,30 +27,54 @@ namespace ESM::Book tableToBook(const sol::table& rec) { ESM::Book book; - book.mName = rec["name"]; - book.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - book.mIcon = rec["icon"]; - book.mText = rec["text"]; - std::string_view enchantId = rec["enchant"].get(); - book.mEnchant = ESM::RefId::deserializeText(enchantId); - - book.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); - std::string_view scriptId = rec["mwscript"].get(); - book.mScript = ESM::RefId::deserializeText(scriptId); - book.mData.mWeight = rec["weight"]; - book.mData.mValue = rec["value"]; - book.mData.mIsScroll = rec["isScroll"]; - book.mRecordFlags = 0; - - ESM::RefId skill = ESM::RefId::deserializeText(rec["skill"].get()); - - book.mData.mSkillId = -1; - if (!skill.empty()) + if (rec["template"] != sol::nil) + book = LuaUtil::cast(rec["template"]); + else { - book.mData.mSkillId = ESM::Skill::refIdToIndex(skill); - if (book.mData.mSkillId == -1) - throw std::runtime_error("Incorrect skill: " + skill.toDebugString()); + book.blank(); + book.mData.mSkillId = -1; } + if (rec["name"] != sol::nil) + book.mName = rec["name"]; + if (rec["model"] != sol::nil) + book.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + book.mIcon = rec["icon"]; + if (rec["text"] != sol::nil) + book.mText = rec["text"]; + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + book.mEnchant = ESM::RefId::deserializeText(enchantId); + } + + if (rec["enchantCapacity"] != sol::nil) + book.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + book.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + book.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + book.mData.mValue = rec["value"]; + if (rec["isScroll"] != sol::nil) + book.mData.mIsScroll = rec["isScroll"] ? 1 : 0; + + if (rec["skill"] != sol::nil) + { + ESM::RefId skill = ESM::RefId::deserializeText(rec["skill"].get()); + + book.mData.mSkillId = -1; + if (!skill.empty()) + { + book.mData.mSkillId = ESM::Skill::refIdToIndex(skill); + if (book.mData.mSkillId == -1) + throw std::runtime_error("Incorrect skill: " + skill.toDebugString()); + } + } + return book; } } @@ -59,9 +83,10 @@ namespace MWLua { void addBookBindings(sol::table book, const Context& context) { + sol::state_view lua = context.sol(); // types.book.SKILL is deprecated (core.SKILL should be used instead) // TODO: Remove book.SKILL after branching 0.49 - sol::table skill(context.mLua->sol(), sol::create); + sol::table skill(lua, sol::create); book["SKILL"] = LuaUtil::makeStrictReadOnly(skill); book["createRecordDraft"] = tableToBook; for (int id = 0; id < ESM::Skill::Length; ++id) @@ -74,23 +99,21 @@ namespace MWLua addRecordFunctionBinding(book, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Book"); + sol::usertype record = lua.new_usertype("ESM3_Book"); record[sol::meta_function::to_string] = [](const ESM::Book& rec) { return "ESM3_Book[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Book& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); - record["mwscript"] - = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mScript.serializeText(); }); + addModelProperty(record); + record["mwscript"] = sol::readonly_property( + [](const ESM::Book& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["icon"] = sol::readonly_property([vfs](const ESM::Book& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); record["text"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mText; }); - record["enchant"] - = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mEnchant.serializeText(); }); + record["enchant"] = sol::readonly_property( + [](const ESM::Book& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mEnchant); }); record["isScroll"] = sol::readonly_property([](const ESM::Book& rec) -> bool { return rec.mData.mIsScroll; }); record["value"] = sol::readonly_property([](const ESM::Book& rec) -> int { return rec.mData.mValue; }); record["weight"] = sol::readonly_property([](const ESM::Book& rec) -> float { return rec.mData.mWeight; }); @@ -98,9 +121,7 @@ namespace MWLua = sol::readonly_property([](const ESM::Book& rec) -> float { return rec.mData.mEnchant * 0.1f; }); record["skill"] = sol::readonly_property([](const ESM::Book& rec) -> sol::optional { ESM::RefId skill = ESM::Skill::indexToRefId(rec.mData.mSkillId); - if (!skill.empty()) - return skill.serializeText(); - return sol::nullopt; + return LuaUtil::serializeRefId(skill); }); } } diff --git a/apps/openmw/mwlua/types/clothing.cpp b/apps/openmw/mwlua/types/clothing.cpp index 9bdd579286..0085f05198 100644 --- a/apps/openmw/mwlua/types/clothing.cpp +++ b/apps/openmw/mwlua/types/clothing.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -22,22 +23,42 @@ namespace ESM::Clothing tableToClothing(const sol::table& rec) { ESM::Clothing clothing; - clothing.mName = rec["name"]; - clothing.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - clothing.mIcon = rec["icon"]; - std::string_view scriptId = rec["mwscript"].get(); - clothing.mScript = ESM::RefId::deserializeText(scriptId); - clothing.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); - std::string_view enchantId = rec["enchant"].get(); - clothing.mEnchant = ESM::RefId::deserializeText(enchantId); - clothing.mData.mWeight = rec["weight"]; - clothing.mData.mValue = rec["value"]; - clothing.mRecordFlags = 0; - int clothingType = rec["type"].get(); - if (clothingType >= 0 && clothingType <= ESM::Clothing::Amulet) - clothing.mData.mType = clothingType; + if (rec["template"] != sol::nil) + clothing = LuaUtil::cast(rec["template"]); else - throw std::runtime_error("Invalid Clothing Type provided: " + std::to_string(clothingType)); + clothing.blank(); + + if (rec["name"] != sol::nil) + clothing.mName = rec["name"]; + if (rec["model"] != sol::nil) + clothing.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + clothing.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + clothing.mScript = ESM::RefId::deserializeText(scriptId); + } + + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + clothing.mEnchant = ESM::RefId::deserializeText(enchantId); + } + if (rec["enchantCapacity"] != sol::nil) + clothing.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + if (rec["weight"] != sol::nil) + clothing.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + clothing.mData.mValue = rec["value"]; + if (rec["type"] != sol::nil) + { + int clothingType = rec["type"].get(); + if (clothingType >= 0 && clothingType <= ESM::Clothing::Amulet) + clothing.mData.mType = clothingType; + else + throw std::runtime_error("Invalid Clothing Type provided: " + std::to_string(clothingType)); + } return clothing; } } @@ -47,39 +68,41 @@ namespace MWLua { clothing["createRecordDraft"] = tableToClothing; - clothing["TYPE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Amulet", ESM::Clothing::Amulet }, - { "Belt", ESM::Clothing::Belt }, - { "LGlove", ESM::Clothing::LGlove }, - { "Pants", ESM::Clothing::Pants }, - { "RGlove", ESM::Clothing::RGlove }, - { "Ring", ESM::Clothing::Ring }, - { "Robe", ESM::Clothing::Robe }, - { "Shirt", ESM::Clothing::Shirt }, - { "Shoes", ESM::Clothing::Shoes }, - { "Skirt", ESM::Clothing::Skirt }, - })); + sol::state_view lua = context.sol(); + clothing["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Amulet", ESM::Clothing::Amulet }, + { "Belt", ESM::Clothing::Belt }, + { "LGlove", ESM::Clothing::LGlove }, + { "Pants", ESM::Clothing::Pants }, + { "RGlove", ESM::Clothing::RGlove }, + { "Ring", ESM::Clothing::Ring }, + { "Robe", ESM::Clothing::Robe }, + { "Shirt", ESM::Clothing::Shirt }, + { "Shoes", ESM::Clothing::Shoes }, + { "Skirt", ESM::Clothing::Skirt }, + })); auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addRecordFunctionBinding(clothing, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Clothing"); + sol::usertype record = lua.new_usertype("ESM3_Clothing"); record[sol::meta_function::to_string] = [](const ESM::Clothing& rec) -> std::string { return "ESM3_Clothing[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Clothing& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Clothing& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Clothing& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); record["icon"] = sol::readonly_property([vfs](const ESM::Clothing& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); - record["enchant"] = sol::readonly_property( - [](const ESM::Clothing& rec) -> std::string { return rec.mEnchant.serializeText(); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Clothing& rec) -> std::string { return rec.mScript.serializeText(); }); + record["enchant"] = sol::readonly_property([](const ESM::Clothing& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mEnchant); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Clothing& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); record["weight"] = sol::readonly_property([](const ESM::Clothing& rec) -> float { return rec.mData.mWeight; }); record["value"] = sol::readonly_property([](const ESM::Clothing& rec) -> int { return rec.mData.mValue; }); record["type"] = sol::readonly_property([](const ESM::Clothing& rec) -> int { return rec.mData.mType; }); diff --git a/apps/openmw/mwlua/types/container.cpp b/apps/openmw/mwlua/types/container.cpp index dd246a73d5..9d3821ca48 100644 --- a/apps/openmw/mwlua/types/container.cpp +++ b/apps/openmw/mwlua/types/container.cpp @@ -1,14 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include -#include +#include "apps/openmw/mwworld/class.hpp" namespace sol { @@ -31,31 +31,34 @@ namespace MWLua container["content"] = sol::overload([](const LObject& o) { return Inventory{ o }; }, [](const GObject& o) { return Inventory{ o }; }); container["inventory"] = container["content"]; - container["encumbrance"] = [](const Object& obj) -> float { + container["getEncumbrance"] = [](const Object& obj) -> float { const MWWorld::Ptr& ptr = containerPtr(obj); return ptr.getClass().getEncumbrance(ptr); }; - container["capacity"] = [](const Object& obj) -> float { + container["encumbrance"] = container["getEncumbrance"]; // for compatibility; should be removed later + container["getCapacity"] = [](const Object& obj) -> float { const MWWorld::Ptr& ptr = containerPtr(obj); return ptr.getClass().getCapacity(ptr); }; - - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + container["capacity"] = container["getCapacity"]; // for compatibility; should be removed later addRecordFunctionBinding(container, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Container"); + sol::usertype record = context.sol().new_usertype("ESM3_Container"); record[sol::meta_function::to_string] = [](const ESM::Container& rec) -> std::string { return "ESM3_Container[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Container& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Container& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Container& rec) -> std::string { return rec.mScript.serializeText(); }); record["weight"] = sol::readonly_property([](const ESM::Container& rec) -> float { return rec.mWeight; }); + record["isOrganic"] = sol::readonly_property( + [](const ESM::Container& rec) -> bool { return rec.mFlags & ESM::Container::Organic; }); + record["isRespawning"] = sol::readonly_property( + [](const ESM::Container& rec) -> bool { return rec.mFlags & ESM::Container::Respawn; }); } } diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp index 08c0dd021c..4ebc658eb9 100644 --- a/apps/openmw/mwlua/types/creature.cpp +++ b/apps/openmw/mwlua/types/creature.cpp @@ -1,14 +1,15 @@ #include "types.hpp" +#include "../stats.hpp" +#include "actor.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include - namespace sol { template <> @@ -21,32 +22,59 @@ namespace MWLua { void addCreatureBindings(sol::table creature, const Context& context) { - creature["TYPE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Creatures", ESM::Creature::Creatures }, - { "Daedra", ESM::Creature::Daedra }, - { "Undead", ESM::Creature::Undead }, - { "Humanoid", ESM::Creature::Humanoid }, - })); - - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + sol::state_view lua = context.sol(); + creature["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Creatures", ESM::Creature::Creatures }, + { "Daedra", ESM::Creature::Daedra }, + { "Undead", ESM::Creature::Undead }, + { "Humanoid", ESM::Creature::Humanoid }, + })); addRecordFunctionBinding(creature, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Creature"); + sol::usertype record = lua.new_usertype("ESM3_Creature"); record[sol::meta_function::to_string] = [](const ESM::Creature& rec) { return "ESM3_Creature[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Creature& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Creature& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Creature& rec) -> std::string { return rec.mScript.serializeText(); }); record["baseCreature"] = sol::readonly_property( [](const ESM::Creature& rec) -> std::string { return rec.mOriginal.serializeText(); }); record["soulValue"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mSoul; }); record["type"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mType; }); record["baseGold"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mGold; }); + record["combatSkill"] + = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mCombat; }); + record["magicSkill"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mMagic; }); + record["stealthSkill"] + = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mStealth; }); + record["attack"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Creature& rec) -> sol::table { + sol::table res(lua, sol::create); + int index = 1; + for (auto attack : rec.mData.mAttack) + res[index++] = attack; + return LuaUtil::makeReadOnly(res); + }); + record["canFly"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Flies; }); + record["canSwim"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Swims; }); + record["canUseWeapons"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Weapon; }); + record["canWalk"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Walks; }); + record["isBiped"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Bipedal; }); + record["isEssential"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; }); + record["isRespawning"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; }); + + addActorServicesBindings(record, context); } } diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index 5a2cfc8aee..58a53a7124 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -1,13 +1,18 @@ #include "types.hpp" +#include "modelproperty.hpp" + +#include "../localscripts.hpp" + #include #include +#include #include #include #include #include -#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/class.hpp" #include "apps/openmw/mwworld/worldmodel.hpp" namespace sol @@ -38,6 +43,47 @@ namespace MWLua void addDoorBindings(sol::table door, const Context& context) { + sol::state_view lua = context.sol(); + door["STATE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Idle", MWWorld::DoorState::Idle }, + { "Opening", MWWorld::DoorState::Opening }, + { "Closing", MWWorld::DoorState::Closing }, + })); + door["getDoorState"] = [](const Object& o) -> MWWorld::DoorState { + const MWWorld::Ptr& door = doorPtr(o); + return door.getClass().getDoorState(door); + }; + door["isOpen"] = [](const Object& o) { + const MWWorld::Ptr& door = doorPtr(o); + bool doorIsIdle = door.getClass().getDoorState(door) == MWWorld::DoorState::Idle; + bool doorIsOpen = door.getRefData().getPosition().rot[2] != door.getCellRef().getPosition().rot[2]; + + return doorIsIdle && doorIsOpen; + }; + door["isClosed"] = [](const Object& o) { + const MWWorld::Ptr& door = doorPtr(o); + bool doorIsIdle = door.getClass().getDoorState(door) == MWWorld::DoorState::Idle; + bool doorIsOpen = door.getRefData().getPosition().rot[2] != door.getCellRef().getPosition().rot[2]; + + return doorIsIdle && !doorIsOpen; + }; + door["activateDoor"] = [](const Object& o, sol::optional openState) { + bool allowChanges + = dynamic_cast(&o) != nullptr || dynamic_cast(&o) != nullptr; + if (!allowChanges) + throw std::runtime_error("Can only be used in global scripts or in local scripts on self."); + + const MWWorld::Ptr& door = doorPtr(o); + auto world = MWBase::Environment::get().getWorld(); + + if (!openState.has_value()) + world->activateDoor(door); + else if (*openState) + world->activateDoor(door, MWWorld::DoorState::Opening); + else + world->activateDoor(door, MWWorld::DoorState::Closing); + }; door["isTeleport"] = [](const Object& o) { return doorPtr(o).getCellRef().getTeleport(); }; door["destPosition"] = [](const Object& o) -> osg::Vec3f { return doorPtr(o).getCellRef().getDoorDest().asVec3(); }; @@ -55,21 +101,17 @@ namespace MWLua return sol::make_object(lua, LCell{ &cell }); }; - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(door, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Door"); + sol::usertype record = lua.new_usertype("ESM3_Door"); record[sol::meta_function::to_string] = [](const ESM::Door& rec) -> std::string { return "ESM3_Door[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Door& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); - record["mwscript"] - = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mScript.serializeText(); }); + addModelProperty(record); + record["mwscript"] = sol::readonly_property( + [](const ESM::Door& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["openSound"] = sol::readonly_property( [](const ESM::Door& rec) -> std::string { return rec.mOpenSound.serializeText(); }); record["closeSound"] = sol::readonly_property( @@ -95,20 +137,16 @@ namespace MWLua return sol::make_object(lua, LCell{ &cell }); }; - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(door, context, "ESM4Door"); - sol::usertype record = context.mLua->sol().new_usertype("ESM4_Door"); + sol::usertype record = context.sol().new_usertype("ESM4_Door"); record[sol::meta_function::to_string] = [](const ESM4::Door& rec) -> std::string { return "ESM4_Door[" + ESM::RefId(rec.mId).toDebugString() + "]"; }; record["id"] = sol::readonly_property( [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mId).serializeText(); }); record["name"] = sol::readonly_property([](const ESM4::Door& rec) -> std::string { return rec.mFullName; }); - record["model"] = sol::readonly_property([vfs](const ESM4::Door& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); record["isAutomatic"] = sol::readonly_property( [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); } diff --git a/apps/openmw/mwlua/types/ingredient.cpp b/apps/openmw/mwlua/types/ingredient.cpp index 31791a19ea..8e52a82b19 100644 --- a/apps/openmw/mwlua/types/ingredient.cpp +++ b/apps/openmw/mwlua/types/ingredient.cpp @@ -1,15 +1,15 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include #include +#include #include #include -#include - -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -26,40 +26,40 @@ namespace MWLua auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addRecordFunctionBinding(ingredient, context); - - sol::usertype record = context.mLua->sol().new_usertype(("ESM3_Ingredient")); + sol::state_view lua = context.sol(); + sol::usertype record = lua.new_usertype(("ESM3_Ingredient")); record[sol::meta_function::to_string] = [](const ESM::Ingredient& rec) { return "ESM3_Ingredient[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Ingredient& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Ingredient& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Ingredient& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Ingredient& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); record["weight"] = sol::readonly_property([](const ESM::Ingredient& rec) -> float { return rec.mData.mWeight; }); record["value"] = sol::readonly_property([](const ESM::Ingredient& rec) -> int { return rec.mData.mValue; }); - record["effects"] = sol::readonly_property([context](const ESM::Ingredient& rec) -> sol::table { - sol::table res(context.mLua->sol(), sol::create); + record["effects"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Ingredient& rec) -> sol::table { + sol::table res(lua, sol::create); for (size_t i = 0; i < 4; ++i) { if (rec.mData.mEffectID[i] < 0) continue; - ESM::ENAMstruct effect; - effect.mEffectID = rec.mData.mEffectID[i]; - effect.mSkill = rec.mData.mSkills[i]; - effect.mAttribute = rec.mData.mAttributes[i]; - effect.mRange = ESM::RT_Self; - effect.mArea = 0; - effect.mDuration = 0; - effect.mMagnMin = 0; - effect.mMagnMax = 0; - res[i + 1] = effect; + ESM::IndexedENAMstruct effect; + effect.mData.mEffectID = rec.mData.mEffectID[i]; + effect.mData.mSkill = rec.mData.mSkills[i]; + effect.mData.mAttribute = rec.mData.mAttributes[i]; + effect.mData.mRange = ESM::RT_Self; + effect.mData.mArea = 0; + effect.mData.mDuration = 0; + effect.mData.mMagnMin = 0; + effect.mData.mMagnMax = 0; + effect.mIndex = i; + res[LuaUtil::toLuaIndex(i)] = effect; } return res; }); diff --git a/apps/openmw/mwlua/types/item.cpp b/apps/openmw/mwlua/types/item.cpp index 2b77b6a7c8..ba995a9808 100644 --- a/apps/openmw/mwlua/types/item.cpp +++ b/apps/openmw/mwlua/types/item.cpp @@ -1,16 +1,32 @@ #include +#include "../../mwmechanics/spellutil.hpp" #include "../../mwworld/class.hpp" +#include "../itemdata.hpp" + #include "types.hpp" namespace MWLua { - void addItemBindings(sol::table item) + void addItemBindings(sol::table item, const Context& context) { - item["getEnchantmentCharge"] - = [](const Object& object) { return object.ptr().getCellRef().getEnchantmentCharge(); }; - item["setEnchantmentCharge"] - = [](const GObject& object, float charge) { object.ptr().getCellRef().setEnchantmentCharge(charge); }; + // Deprecated. Moved to itemData; should be removed later + item["getEnchantmentCharge"] = [](const Object& object) -> sol::optional { + float charge = object.ptr().getCellRef().getEnchantmentCharge(); + if (charge == -1) + return sol::nullopt; + else + return charge; + }; + item["setEnchantmentCharge"] = [](const GObject& object, sol::optional charge) { + object.ptr().getCellRef().setEnchantmentCharge(charge.value_or(-1)); + }; + item["isRestocking"] + = [](const Object& object) -> bool { return object.ptr().getCellRef().getCount(false) < 0; }; + + item["isCarriable"] = [](const Object& object) -> bool { return object.ptr().getClass().isItem(object.ptr()); }; + + addItemDataBindings(item, context); } } diff --git a/apps/openmw/mwlua/types/levelledlist.cpp b/apps/openmw/mwlua/types/levelledlist.cpp index cb3bd7a6bc..fd848d9121 100644 --- a/apps/openmw/mwlua/types/levelledlist.cpp +++ b/apps/openmw/mwlua/types/levelledlist.cpp @@ -1,6 +1,7 @@ #include "types.hpp" #include +#include #include "../../mwbase/environment.hpp" #include "../../mwbase/world.hpp" @@ -22,7 +23,7 @@ namespace MWLua { void addLevelledCreatureBindings(sol::table list, const Context& context) { - auto& state = context.mLua->sol(); + auto state = context.sol(); auto item = state.new_usertype("ESM3_LevelledListItem"); item["id"] = sol::readonly_property( [](const ESM::LevelledListBase::LevelItem& rec) -> std::string { return rec.mId.serializeText(); }); @@ -45,7 +46,7 @@ namespace MWLua record["creatures"] = sol::readonly_property([&](const ESM::CreatureLevList& rec) -> sol::table { sol::table res(state, sol::create); for (size_t i = 0; i < rec.mList.size(); ++i) - res[i + 1] = rec.mList[i]; + res[LuaUtil::toLuaIndex(i)] = rec.mList[i]; return res; }); record["calculateFromAllLevels"] = sol::readonly_property( diff --git a/apps/openmw/mwlua/types/light.cpp b/apps/openmw/mwlua/types/light.cpp index 347bb61641..41a0bf8d4a 100644 --- a/apps/openmw/mwlua/types/light.cpp +++ b/apps/openmw/mwlua/types/light.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -17,6 +18,65 @@ namespace sol }; } +namespace +{ + void setRecordFlag(const sol::table& rec, const std::string& key, int flag, ESM::Light& record) + { + if (auto luaFlag = rec[key]; luaFlag != sol::nil) + { + if (luaFlag) + { + record.mData.mFlags |= flag; + } + else + { + record.mData.mFlags &= ~flag; + } + } + } + // Populates a light struct from a Lua table. + ESM::Light tableToLight(const sol::table& rec) + { + ESM::Light light; + if (rec["template"] != sol::nil) + light = LuaUtil::cast(rec["template"]); + else + light.blank(); + if (rec["name"] != sol::nil) + light.mName = rec["name"]; + if (rec["model"] != sol::nil) + light.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + light.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + light.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + light.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + light.mData.mValue = rec["value"]; + if (rec["duration"] != sol::nil) + light.mData.mTime = rec["duration"]; + if (rec["radius"] != sol::nil) + light.mData.mRadius = rec["radius"]; + if (rec["color"] != sol::nil) + light.mData.mColor = rec["color"]; + setRecordFlag(rec, "isCarriable", ESM::Light::Carry, light); + setRecordFlag(rec, "isDynamic", ESM::Light::Dynamic, light); + setRecordFlag(rec, "isFire", ESM::Light::Fire, light); + setRecordFlag(rec, "isFlicker", ESM::Light::Flicker, light); + setRecordFlag(rec, "isFlickerSlow", ESM::Light::FlickerSlow, light); + setRecordFlag(rec, "isNegative", ESM::Light::Negative, light); + setRecordFlag(rec, "isOffByDefault", ESM::Light::OffDefault, light); + setRecordFlag(rec, "isPulse", ESM::Light::Pulse, light); + setRecordFlag(rec, "isPulseSlow", ESM::Light::PulseSlow, light); + + return light; + } +} + namespace MWLua { void addLightBindings(sol::table light, const Context& context) @@ -24,23 +84,22 @@ namespace MWLua auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addRecordFunctionBinding(light, context); + light["createRecordDraft"] = tableToLight; - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Light"); + sol::usertype record = context.sol().new_usertype("ESM3_Light"); record[sol::meta_function::to_string] = [](const ESM::Light& rec) -> std::string { return "ESM3_Light[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Light& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); record["icon"] = sol::readonly_property([vfs](const ESM::Light& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); record["sound"] = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mSound.serializeText(); }); - record["mwscript"] - = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mScript.serializeText(); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Light& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["weight"] = sol::readonly_property([](const ESM::Light& rec) -> float { return rec.mData.mWeight; }); record["value"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mValue; }); record["duration"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mTime; }); @@ -48,5 +107,21 @@ namespace MWLua record["color"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mColor; }); record["isCarriable"] = sol::readonly_property( [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Carry; }); + record["isDynamic"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Dynamic; }); + record["isFire"] + = sol::readonly_property([](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Fire; }); + record["isFlicker"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Flicker; }); + record["isFlickerSlow"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::FlickerSlow; }); + record["isNegative"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Negative; }); + record["isOffByDefault"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::OffDefault; }); + record["isPulse"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Pulse; }); + record["isPulseSlow"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::PulseSlow; }); } } diff --git a/apps/openmw/mwlua/types/lockable.cpp b/apps/openmw/mwlua/types/lockable.cpp index e7413edef5..2569f42ee4 100644 --- a/apps/openmw/mwlua/types/lockable.cpp +++ b/apps/openmw/mwlua/types/lockable.cpp @@ -1,11 +1,11 @@ - #include "types.hpp" + #include #include #include #include -#include +#include "apps/openmw/mwworld/esmstore.hpp" namespace MWLua { diff --git a/apps/openmw/mwlua/types/lockpick.cpp b/apps/openmw/mwlua/types/lockpick.cpp index 786471461a..3f41c65df2 100644 --- a/apps/openmw/mwlua/types/lockpick.cpp +++ b/apps/openmw/mwlua/types/lockpick.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -25,17 +26,16 @@ namespace MWLua addRecordFunctionBinding(lockpick, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Lockpick"); + sol::usertype record = context.sol().new_usertype("ESM3_Lockpick"); record[sol::meta_function::to_string] = [](const ESM::Lockpick& rec) { return "ESM3_Lockpick[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Lockpick& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Lockpick& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/misc.cpp b/apps/openmw/mwlua/types/misc.cpp index 461444abff..ef89464a2c 100644 --- a/apps/openmw/mwlua/types/misc.cpp +++ b/apps/openmw/mwlua/types/misc.cpp @@ -1,14 +1,16 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" namespace sol { @@ -24,15 +26,25 @@ namespace ESM::Miscellaneous tableToMisc(const sol::table& rec) { ESM::Miscellaneous misc; - misc.mName = rec["name"]; - misc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - misc.mIcon = rec["icon"]; - std::string_view scriptId = rec["mwscript"].get(); - misc.mScript = ESM::RefId::deserializeText(scriptId); - misc.mData.mWeight = rec["weight"]; - misc.mData.mValue = rec["value"]; - misc.mData.mFlags = 0; - misc.mRecordFlags = 0; + if (rec["template"] != sol::nil) + misc = LuaUtil::cast(rec["template"]); + else + misc.blank(); + if (rec["name"] != sol::nil) + misc.mName = rec["name"]; + if (rec["model"] != sol::nil) + misc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + misc.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + misc.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + misc.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + misc.mData.mValue = rec["value"]; return misc; } } @@ -46,6 +58,7 @@ namespace MWLua addRecordFunctionBinding(miscellaneous, context); miscellaneous["createRecordDraft"] = tableToMisc; + // Deprecated. Moved to itemData; should be removed later miscellaneous["setSoul"] = [](const GObject& object, std::string_view soulId) { ESM::RefId creature = ESM::RefId::deserializeText(soulId); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -60,24 +73,20 @@ namespace MWLua }; miscellaneous["getSoul"] = [](const Object& object) -> sol::optional { ESM::RefId soul = object.ptr().getCellRef().getSoul(); - if (soul.empty()) - return sol::nullopt; - else - return soul.serializeText(); + return LuaUtil::serializeRefId(soul); }; miscellaneous["soul"] = miscellaneous["getSoul"]; // for compatibility; should be removed later - sol::usertype record - = context.mLua->sol().new_usertype("ESM3_Miscellaneous"); + + sol::usertype record = context.sol().new_usertype("ESM3_Miscellaneous"); record[sol::meta_function::to_string] = [](const ESM::Miscellaneous& rec) { return "ESM3_Miscellaneous[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property( [](const ESM::Miscellaneous& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Miscellaneous& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); + addModelProperty(record); + record["mwscript"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); }); - record["mwscript"] = sol::readonly_property( - [](const ESM::Miscellaneous& rec) -> std::string { return rec.mScript.serializeText(); }); record["icon"] = sol::readonly_property([vfs](const ESM::Miscellaneous& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/modelproperty.hpp b/apps/openmw/mwlua/types/modelproperty.hpp new file mode 100644 index 0000000000..7bb473ad81 --- /dev/null +++ b/apps/openmw/mwlua/types/modelproperty.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_APPS_OPENMW_MWLUA_TYPES_MODELPROPERTY_H +#define OPENMW_APPS_OPENMW_MWLUA_TYPES_MODELPROPERTY_H + +#include +#include + +#include +#include + +namespace MWLua +{ + template + void addModelProperty(sol::usertype& recordType) + { + recordType["model"] = sol::readonly_property([](const T& recordValue) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(recordValue.mModel)).value(); + }); + } +} + +#endif diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 06bcab243b..7cfcf6d704 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -1,14 +1,24 @@ #include "types.hpp" +#include "actor.hpp" +#include "modelproperty.hpp" + +#include #include #include +#include +#include -#include -#include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/mechanicsmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/npcstats.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "../classbindings.hpp" +#include "../localscripts.hpp" +#include "../racebindings.hpp" #include "../stats.hpp" namespace sol @@ -19,6 +29,44 @@ namespace sol }; } +namespace +{ + size_t getValidRanksCount(const ESM::Faction* faction) + { + if (!faction) + return 0; + + for (size_t i = 0; i < faction->mRanks.size(); i++) + { + if (faction->mRanks[i].empty()) + return i; + } + + return faction->mRanks.size(); + } + + ESM::RefId parseFactionId(std::string_view faction) + { + ESM::RefId id = ESM::RefId::deserializeText(faction); + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + if (!store->get().search(id)) + throw std::runtime_error("Faction '" + std::string(faction) + "' does not exist"); + return id; + } + + void verifyPlayer(const MWLua::Object& o) + { + if (o.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("The argument must be a player!"); + } + + void verifyNpc(const MWWorld::Class& cls) + { + if (!cls.isNpc()) + throw std::runtime_error("The argument must be a NPC!"); + } +} + namespace MWLua { void addNpcBindings(sol::table npc, const Context& context) @@ -27,7 +75,9 @@ namespace MWLua addRecordFunctionBinding(npc, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_NPC"); + sol::state_view lua = context.sol(); + + sol::usertype record = lua.new_usertype("ESM3_NPC"); record[sol::meta_function::to_string] = [](const ESM::NPC& rec) { return "ESM3_NPC[" + rec.mId.toDebugString() + "]"; }; record["id"] @@ -37,14 +87,25 @@ namespace MWLua = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mRace.serializeText(); }); record["class"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mClass.serializeText(); }); - record["mwscript"] - = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mScript.serializeText(); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::NPC& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["hair"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHair.serializeText(); }); + record["baseDisposition"] + = sol::readonly_property([](const ESM::NPC& rec) -> int { return (int)rec.mNpdt.mDisposition; }); record["head"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); }); + addModelProperty(record); + record["isEssential"] + = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; }); record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); }); + record["isRespawning"] + = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; }); + addActorServicesBindings(record, context); + + npc["classes"] = initClassRecordBindings(context); + npc["races"] = initRaceRecordBindings(context); // This function is game-specific, in future we should replace it with something more universal. npc["isWerewolf"] = [](const Object& o) { @@ -54,5 +115,247 @@ namespace MWLua else throw std::runtime_error("NPC or Player expected"); }; + + npc["getDisposition"] = [](const Object& o, const Object& player) -> int { + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + return MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(o.ptr()); + }; + + npc["getBaseDisposition"] = [](const Object& o, const Object& player) -> int { + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + return cls.getNpcStats(o.ptr()).getBaseDisposition(); + }; + + npc["setBaseDisposition"] = [](Object& o, const Object& player, int value) { + if (dynamic_cast(&o) && !dynamic_cast(&o)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + cls.getNpcStats(o.ptr()).setBaseDisposition(value); + }; + + npc["modifyBaseDisposition"] = [](Object& o, const Object& player, int value) { + if (dynamic_cast(&o) && !dynamic_cast(&o)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + auto& stats = cls.getNpcStats(o.ptr()); + stats.setBaseDisposition(stats.getBaseDisposition() + value); + }; + + npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t { + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + const MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + if (npcStats.isInFaction(factionId)) + { + int factionRank = npcStats.getFactionRank(factionId); + return LuaUtil::toLuaIndex(factionRank); + } + } + else + { + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId == primaryFactionId) + return LuaUtil::toLuaIndex(ptr.getClass().getPrimaryFactionRank(ptr)); + } + return 0; + }; + + npc["setFactionRank"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + const ESM::Faction* factionPtr + = MWBase::Environment::get().getESMStore()->get().find(factionId); + + auto ranksCount = static_cast(getValidRanksCount(factionPtr)); + if (value <= 0 || value > ranksCount) + throw std::runtime_error("Requested rank does not exist"); + + auto targetRank = LuaUtil::fromLuaIndex(std::clamp(value, 1, ranksCount)); + + if (ptr != MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId != primaryFactionId) + throw std::runtime_error("Only players can modify ranks in non-primary factions"); + } + + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + if (!npcStats.isInFaction(factionId)) + throw std::runtime_error("Target actor is not a member of faction " + factionId.toDebugString()); + + npcStats.setFactionRank(factionId, targetRank); + }; + + npc["modifyFactionRank"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + if (value == 0) + return; + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + const ESM::Faction* factionPtr + = MWBase::Environment::get().getESMStore()->get().search(factionId); + if (!factionPtr) + return; + + auto ranksCount = static_cast(getValidRanksCount(factionPtr)); + + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + int currentRank = npcStats.getFactionRank(factionId); + if (currentRank >= 0) + npcStats.setFactionRank(factionId, std::clamp(currentRank + value, 0, ranksCount - 1)); + else + throw std::runtime_error("Target actor is not a member of faction " + factionId.toDebugString()); + + return; + } + + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId != primaryFactionId) + throw std::runtime_error("Only players can modify ranks in non-primary factions"); + + // If we already changed rank for this NPC, modify current rank in the NPC stats. + // Otherwise take rank from base NPC record, adjust it and put it to NPC data. + int currentRank = npcStats.getFactionRank(factionId); + if (currentRank < 0) + { + currentRank = ptr.getClass().getPrimaryFactionRank(ptr); + npcStats.joinFaction(factionId); + } + + npcStats.setFactionRank(factionId, std::clamp(currentRank + value, 0, ranksCount - 1)); + }; + + npc["joinFaction"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + int currentRank = npcStats.getFactionRank(factionId); + if (currentRank < 0) + npcStats.joinFaction(factionId); + return; + } + + throw std::runtime_error("Only player can join factions"); + }; + + npc["leaveFaction"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + ptr.getClass().getNpcStats(ptr).setFactionRank(factionId, -1); + return; + } + + throw std::runtime_error("Only player can leave factions"); + }; + + npc["getFactionReputation"] = [](const Object& actor, std::string_view faction) { + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + return ptr.getClass().getNpcStats(ptr).getFactionReputation(factionId); + }; + + npc["setFactionReputation"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + ptr.getClass().getNpcStats(ptr).setFactionReputation(factionId, value); + }; + + npc["modifyFactionReputation"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + int existingReputation = npcStats.getFactionReputation(factionId); + npcStats.setFactionReputation(factionId, existingReputation + value); + }; + + npc["expel"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + ptr.getClass().getNpcStats(ptr).expell(factionId, false); + }; + npc["clearExpelled"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + ptr.getClass().getNpcStats(ptr).clearExpelled(factionId); + }; + npc["isExpelled"] = [](const Object& actor, std::string_view faction) { + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + return ptr.getClass().getNpcStats(ptr).getExpelled(factionId); + }; + npc["getFactions"] = [](sol::this_state lua, const Object& actor) { + const MWWorld::Ptr ptr = actor.ptr(); + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + sol::table res(lua, sol::create); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + for (const auto& [factionId, _] : npcStats.getFactionRanks()) + res.add(factionId.serializeText()); + return res; + } + + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (primaryFactionId.empty()) + return res; + + res.add(primaryFactionId.serializeText()); + + return res; + }; + npc["getCapacity"] = [](const Object& actor) -> float { + const MWWorld::Ptr ptr = actor.ptr(); + return ptr.getClass().getCapacity(ptr); + }; } } diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index 5e44b47f1a..15dc719f2e 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -1,11 +1,20 @@ #include "types.hpp" +#include +#include + +#include "../birthsignbindings.hpp" #include "../luamanagerimp.hpp" -#include -#include -#include -#include -#include + +#include "apps/openmw/mwbase/inputmanager.hpp" +#include "apps/openmw/mwbase/journal.hpp" +#include "apps/openmw/mwbase/mechanicsmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/npcstats.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/globals.hpp" +#include "apps/openmw/mwworld/player.hpp" namespace MWLua { @@ -32,29 +41,62 @@ namespace sol }; } +namespace +{ + ESM::RefId toBirthSignId(const sol::object& recordOrId) + { + if (recordOrId.is()) + return recordOrId.as()->mId; + std::string_view textId = LuaUtil::cast(recordOrId); + ESM::RefId id = ESM::RefId::deserializeText(textId); + if (!MWBase::Environment::get().getESMStore()->get().search(id)) + throw std::runtime_error("Failed to find birth sign: " + std::string(textId)); + return id; + } + + ESM::RefId parseFactionId(std::string_view faction) + { + ESM::RefId id = ESM::RefId::deserializeText(faction); + if (!MWBase::Environment::get().getESMStore()->get().search(id)) + return ESM::RefId(); + return id; + } +} + namespace MWLua { + static void verifyPlayer(const Object& player) + { + if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("The argument must be a player!"); + } - void addPlayerQuestBindings(sol::table& player, const Context& context) + static void verifyNpc(const MWWorld::Class& cls) + { + if (!cls.isNpc()) + throw std::runtime_error("The argument must be a NPC!"); + } + + void addPlayerBindings(sol::table player, const Context& context) { MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); player["quests"] = [](const Object& player) { - if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) - throw std::runtime_error("The argument must be a player!"); + verifyPlayer(player); bool allowChanges = dynamic_cast(&player) != nullptr || dynamic_cast(&player) != nullptr; return Quests{ .mMutable = allowChanges }; }; - sol::usertype quests = context.mLua->sol().new_usertype("Quests"); + sol::state_view lua = context.sol(); + sol::usertype quests = lua.new_usertype("Quests"); quests[sol::meta_function::to_string] = [](const Quests& quests) { return "Quests"; }; - quests[sol::meta_function::index] = sol::overload([](const Quests& quests, std::string_view questId) -> Quest { + quests[sol::meta_function::index] = [](const Quests& quests, std::string_view questId) -> sol::optional { ESM::RefId quest = ESM::RefId::deserializeText(questId); - const ESM::Dialogue* dial = MWBase::Environment::get().getESMStore()->get().find(quest); - if (dial->mType != ESM::Dialogue::Journal) - throw std::runtime_error("Not a quest:" + std::string(questId)); + const ESM::Dialogue* dial = MWBase::Environment::get().getESMStore()->get().search(quest); + if (dial == nullptr || dial->mType != ESM::Dialogue::Journal) + return sol::nullopt; return Quest{ .mQuestId = quest, .mMutable = quests.mMutable }; - }); + }; quests[sol::meta_function::pairs] = [journal](const Quests& quests) { std::vector ids; for (auto it = journal->questBegin(); it != journal->questEnd(); ++it) @@ -69,7 +111,7 @@ namespace MWLua }; }; - sol::usertype quest = context.mLua->sol().new_usertype("Quest"); + sol::usertype quest = lua.new_usertype("Quest"); quest[sol::meta_function::to_string] = [](const Quest& quest) { return "Quest[" + quest.mQuestId.serializeText() + "]"; }; @@ -111,7 +153,7 @@ namespace MWLua // The journal mwscript function has a try function here, we will make the lua function throw an // error. However, the addAction will cause it to error outside of this function. context.mLuaManager->addAction( - [actor, q, stage] { + [actor = std::move(actor), q, stage] { MWWorld::Ptr actorPtr; if (actor) actorPtr = actor->ptr(); @@ -119,17 +161,102 @@ namespace MWLua }, "addJournalEntryAction"); }; - } - void addPlayerBindings(sol::table player, const Context& context) - { + player["CONTROL_SWITCH"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Controls", "playercontrols" }, + { "Fighting", "playerfighting" }, + { "Jumping", "playerjumping" }, + { "Looking", "playerlooking" }, + { "Magic", "playermagic" }, + { "ViewMode", "playerviewswitch" }, + { "VanityMode", "vanitymode" }, + })); + + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + player["getControlSwitch"] = [input](const Object& player, std::string_view key) { + verifyPlayer(player); + return input->getControlSwitch(key); + }; + player["setControlSwitch"] = [input](const Object& player, std::string_view key, bool v) { + verifyPlayer(player); + if (dynamic_cast(&player) && !dynamic_cast(&player)) + throw std::runtime_error("Only player and global scripts can toggle control switches."); + input->toggleControlSwitch(key, v); + }; + player["isTeleportingEnabled"] = [](const Object& player) -> bool { + verifyPlayer(player); + return MWBase::Environment::get().getWorld()->isTeleportingEnabled(); + }; + player["setTeleportingEnabled"] = [](const Object& player, bool state) { + verifyPlayer(player); + if (dynamic_cast(&player) && !dynamic_cast(&player)) + throw std::runtime_error("Only player and global scripts can toggle teleportation."); + MWBase::Environment::get().getWorld()->enableTeleporting(state); + }; + player["sendMenuEvent"] = [context](const Object& player, std::string eventName, const sol::object& eventData) { + verifyPlayer(player); + context.mLuaEvents->addMenuEvent({ std::move(eventName), LuaUtil::serialize(eventData) }); + }; + player["getCrimeLevel"] = [](const Object& o) -> int { const MWWorld::Class& cls = o.ptr().getClass(); return cls.getNpcStats(o.ptr()).getBounty(); }; + player["setCrimeLevel"] = [](const Object& o, int amount) { + verifyPlayer(o); + if (!dynamic_cast(&o)) + throw std::runtime_error("Only global scripts can change crime level"); + const MWWorld::Class& cls = o.ptr().getClass(); + cls.getNpcStats(o.ptr()).setBounty(amount); + if (amount == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + }; player["isCharGenFinished"] = [](const Object&) -> bool { return MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1; }; - addPlayerQuestBindings(player, context); + + player["OFFENSE_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(context.sol(), + { { "Theft", MWBase::MechanicsManager::OffenseType::OT_Theft }, + { "Assault", MWBase::MechanicsManager::OffenseType::OT_Assault }, + { "Murder", MWBase::MechanicsManager::OffenseType::OT_Murder }, + { "Trespassing", MWBase::MechanicsManager::OffenseType::OT_Trespassing }, + { "SleepingInOwnedBed", MWBase::MechanicsManager::OffenseType::OT_SleepingInOwnedBed }, + { "Pickpocket", MWBase::MechanicsManager::OffenseType::OT_Pickpocket } })); + player["_runStandardCommitCrime"] = [](const Object& o, const sol::optional victim, int type, + std::string_view faction, int arg = 0, bool victimAware = false) { + verifyPlayer(o); + if (victim.has_value() && !victim->ptrOrEmpty().isEmpty()) + verifyNpc(victim->ptrOrEmpty().getClass()); + if (!dynamic_cast(&o)) + throw std::runtime_error("Only global scripts can commit crime"); + if (type < 0 || type > MWBase::MechanicsManager::OffenseType::OT_Pickpocket) + throw std::runtime_error("Invalid offense type"); + + ESM::RefId factionId = parseFactionId(faction); + // If the faction is provided but not found, error out + if (faction != "" && factionId == ESM::RefId()) + throw std::runtime_error("Faction does not exist"); + + MWWorld::Ptr victimObj = nullptr; + if (victim.has_value()) + victimObj = victim->ptrOrEmpty(); + return MWBase::Environment::get().getMechanicsManager()->commitCrime(o.ptr(), victimObj, + static_cast(type), factionId, arg, victimAware); + }; + + player["birthSigns"] = initBirthSignRecordBindings(context); + player["getBirthSign"] = [](const Object& player) -> std::string { + verifyPlayer(player); + return MWBase::Environment::get().getWorld()->getPlayer().getBirthSign().serializeText(); + }; + player["setBirthSign"] = [](const Object& player, const sol::object& recordOrId) { + verifyPlayer(player); + if (!dynamic_cast(&player)) + throw std::runtime_error("Only global scripts can change birth signs"); + MWBase::Environment::get().getWorld()->getPlayer().setBirthSign(toBirthSignId(recordOrId)); + }; } } diff --git a/apps/openmw/mwlua/types/potion.cpp b/apps/openmw/mwlua/types/potion.cpp index badf611bc7..4d04c8bb13 100644 --- a/apps/openmw/mwlua/types/potion.cpp +++ b/apps/openmw/mwlua/types/potion.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -23,20 +24,36 @@ namespace ESM::Potion tableToPotion(const sol::table& rec) { ESM::Potion potion; - potion.mName = rec["name"]; - potion.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - potion.mIcon = rec["icon"]; - std::string_view scriptId = rec["mwscript"].get(); - potion.mScript = ESM::RefId::deserializeText(scriptId); - potion.mData.mWeight = rec["weight"]; - potion.mData.mValue = rec["value"]; - potion.mData.mAutoCalc = 0; - potion.mRecordFlags = 0; - sol::table effectsTable = rec["effects"]; - size_t numEffects = effectsTable.size(); - potion.mEffects.mList.resize(numEffects); - for (size_t i = 0; i < numEffects; ++i) - potion.mEffects.mList[i] = LuaUtil::cast(effectsTable[i + 1]); + if (rec["template"] != sol::nil) + potion = LuaUtil::cast(rec["template"]); + else + potion.blank(); + if (rec["name"] != sol::nil) + potion.mName = rec["name"]; + if (rec["model"] != sol::nil) + potion.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + potion.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + potion.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + potion.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + potion.mData.mValue = rec["value"]; + if (rec["effects"] != sol::nil) + { + sol::table effectsTable = rec["effects"]; + size_t numEffects = effectsTable.size(); + potion.mEffects.mList.resize(numEffects); + for (size_t i = 0; i < numEffects; ++i) + { + potion.mEffects.mList[i] = LuaUtil::cast(effectsTable[LuaUtil::toLuaIndex(i)]); + } + potion.mEffects.updateIndexes(); + } return potion; } } @@ -54,26 +71,25 @@ namespace MWLua potion["createRecordDraft"] = tableToPotion; auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Potion"); + sol::state_view lua = context.sol(); + sol::usertype record = lua.new_usertype("ESM3_Potion"); record[sol::meta_function::to_string] = [](const ESM::Potion& rec) { return "ESM3_Potion[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Potion& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); record["icon"] = sol::readonly_property([vfs](const ESM::Potion& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); - record["mwscript"] - = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mScript.serializeText(); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Potion& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["weight"] = sol::readonly_property([](const ESM::Potion& rec) -> float { return rec.mData.mWeight; }); record["value"] = sol::readonly_property([](const ESM::Potion& rec) -> int { return rec.mData.mValue; }); - record["effects"] = sol::readonly_property([context](const ESM::Potion& rec) -> sol::table { - sol::table res(context.mLua->sol(), sol::create); + record["effects"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Potion& rec) -> sol::table { + sol::table res(lua, sol::create); for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) - res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) + res[LuaUtil::toLuaIndex(i)] = rec.mEffects.mList[i]; // ESM::IndexedENAMstruct (effect params) return res; }); } diff --git a/apps/openmw/mwlua/types/probe.cpp b/apps/openmw/mwlua/types/probe.cpp index 668e58c98c..4467f6617a 100644 --- a/apps/openmw/mwlua/types/probe.cpp +++ b/apps/openmw/mwlua/types/probe.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -25,17 +26,15 @@ namespace MWLua addRecordFunctionBinding(probe, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Probe"); + sol::usertype record = context.sol().new_usertype("ESM3_Probe"); record[sol::meta_function::to_string] = [](const ESM::Probe& rec) { return "ESM3_Probe[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); - record["mwscript"] - = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mScript.serializeText(); }); + addModelProperty(record); + record["mwscript"] = sol::readonly_property( + [](const ESM::Probe& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["icon"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/repair.cpp b/apps/openmw/mwlua/types/repair.cpp index 75d0a17c49..af95d187a8 100644 --- a/apps/openmw/mwlua/types/repair.cpp +++ b/apps/openmw/mwlua/types/repair.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -25,17 +26,15 @@ namespace MWLua addRecordFunctionBinding(repair, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Repair"); + sol::usertype record = context.sol().new_usertype("ESM3_Repair"); record[sol::meta_function::to_string] = [](const ESM::Repair& rec) { return "ESM3_Repair[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); - record["mwscript"] - = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mScript.serializeText(); }); + addModelProperty(record); + record["mwscript"] = sol::readonly_property( + [](const ESM::Repair& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["icon"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); diff --git a/apps/openmw/mwlua/types/static.cpp b/apps/openmw/mwlua/types/static.cpp index 76dac4fa00..7a4c0866eb 100644 --- a/apps/openmw/mwlua/types/static.cpp +++ b/apps/openmw/mwlua/types/static.cpp @@ -1,14 +1,12 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include #include #include -#include -#include -#include - namespace sol { template <> @@ -21,17 +19,13 @@ namespace MWLua { void addStaticBindings(sol::table stat, const Context& context) { - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(stat, context); - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Static"); + sol::usertype record = context.sol().new_usertype("ESM3_Static"); record[sol::meta_function::to_string] = [](const ESM::Static& rec) -> std::string { return "ESM3_Static[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Static& rec) -> std::string { return rec.mId.serializeText(); }); - record["model"] = sol::readonly_property([vfs](const ESM::Static& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); } } diff --git a/apps/openmw/mwlua/types/terminal.cpp b/apps/openmw/mwlua/types/terminal.cpp index b0f8e3be0b..8abd52da74 100644 --- a/apps/openmw/mwlua/types/terminal.cpp +++ b/apps/openmw/mwlua/types/terminal.cpp @@ -1,13 +1,13 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include #include #include #include -#include "apps/openmw/mwworld/esmstore.hpp" - namespace sol { template <> @@ -21,12 +21,9 @@ namespace MWLua void addESM4TerminalBindings(sol::table term, const Context& context) { - - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - addRecordFunctionBinding(term, context, "ESM4Terminal"); - sol::usertype record = context.mLua->sol().new_usertype("ESM4_Terminal"); + sol::usertype record = context.sol().new_usertype("ESM4_Terminal"); record[sol::meta_function::to_string] = [](const ESM4::Terminal& rec) -> std::string { return "ESM4_Terminal[" + ESM::RefId(rec.mId).toDebugString() + "]"; }; @@ -38,8 +35,6 @@ namespace MWLua record["resultText"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mResultText; }); record["name"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mFullName; }); - record["model"] = sol::readonly_property([vfs](const ESM4::Terminal& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); } } diff --git a/apps/openmw/mwlua/types/types.cpp b/apps/openmw/mwlua/types/types.cpp index 6095053eee..c7b0b6e9c4 100644 --- a/apps/openmw/mwlua/types/types.cpp +++ b/apps/openmw/mwlua/types/types.cpp @@ -42,12 +42,16 @@ namespace MWLua constexpr std::string_view ESM4Clothing = "ESM4Clothing"; constexpr std::string_view ESM4Container = "ESM4Container"; constexpr std::string_view ESM4Door = "ESM4Door"; + constexpr std::string_view ESM4Flora = "ESM4Flora"; constexpr std::string_view ESM4Furniture = "ESM4Furniture"; constexpr std::string_view ESM4Ingredient = "ESM4Ingredient"; + constexpr std::string_view ESM4ItemMod = "ESM4ItemMod"; constexpr std::string_view ESM4Light = "ESM4Light"; constexpr std::string_view ESM4MiscItem = "ESM4Miscellaneous"; + constexpr std::string_view ESM4MovableStatic = "ESM4MovableStatic"; constexpr std::string_view ESM4Potion = "ESM4Potion"; constexpr std::string_view ESM4Static = "ESM4Static"; + constexpr std::string_view ESM4StaticCollection = "ESM4StaticCollection"; constexpr std::string_view ESM4Terminal = "ESM4Terminal"; constexpr std::string_view ESM4Tree = "ESM4Tree"; constexpr std::string_view ESM4Weapon = "ESM4Weapon"; @@ -85,12 +89,16 @@ namespace MWLua { ESM::REC_CLOT4, ObjectTypeName::ESM4Clothing }, { ESM::REC_CONT4, ObjectTypeName::ESM4Container }, { ESM::REC_DOOR4, ObjectTypeName::ESM4Door }, + { ESM::REC_FLOR4, ObjectTypeName::ESM4Flora }, { ESM::REC_FURN4, ObjectTypeName::ESM4Furniture }, { ESM::REC_INGR4, ObjectTypeName::ESM4Ingredient }, + { ESM::REC_IMOD4, ObjectTypeName::ESM4ItemMod }, { ESM::REC_LIGH4, ObjectTypeName::ESM4Light }, { ESM::REC_MISC4, ObjectTypeName::ESM4MiscItem }, + { ESM::REC_MSTT4, ObjectTypeName::ESM4MovableStatic }, { ESM::REC_ALCH4, ObjectTypeName::ESM4Potion }, { ESM::REC_STAT4, ObjectTypeName::ESM4Static }, + { ESM::REC_SCOL4, ObjectTypeName::ESM4StaticCollection }, { ESM::REC_TERM4, ObjectTypeName::ESM4Terminal }, { ESM::REC_TREE4, ObjectTypeName::ESM4Tree }, { ESM::REC_WEAP4, ObjectTypeName::ESM4Weapon }, @@ -157,18 +165,22 @@ namespace MWLua sol::table initTypesPackage(const Context& context) { - auto* lua = context.mLua; - sol::table types(lua->sol(), sol::create); + auto lua = context.sol(); + + if (lua["openmw_types"] != sol::nil) + return lua["openmw_types"]; + + sol::table types(lua, sol::create); auto addType = [&](std::string_view name, std::vector recTypes, std::optional base = std::nullopt) -> sol::table { - sol::table t(lua->sol(), sol::create); + sol::table t(lua, sol::create); sol::table ro = LuaUtil::makeReadOnly(t); sol::table meta = ro[sol::metatable_key]; meta[sol::meta_function::to_string] = [name]() { return name; }; if (base) { t["baseType"] = types[*base]; - sol::table baseMeta(lua->sol(), sol::create); + sol::table baseMeta(lua, sol::create); baseMeta[sol::meta_function::index] = LuaUtil::getMutableFromReadOnly(types[*base]); t[sol::metatable_key] = baseMeta; } @@ -185,9 +197,11 @@ namespace MWLua addActorBindings( addType(ObjectTypeName::Actor, { ESM::REC_INTERNAL_PLAYER, ESM::REC_CREA, ESM::REC_NPC_ }), context); - addItemBindings(addType(ObjectTypeName::Item, - { ESM::REC_ARMO, ESM::REC_BOOK, ESM::REC_CLOT, ESM::REC_INGR, ESM::REC_LIGH, ESM::REC_MISC, ESM::REC_ALCH, - ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA })); + addItemBindings( + addType(ObjectTypeName::Item, + { ESM::REC_ARMO, ESM::REC_BOOK, ESM::REC_CLOT, ESM::REC_INGR, ESM::REC_LIGH, ESM::REC_MISC, + ESM::REC_ALCH, ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA }), + context); addLockableBindings( addType(ObjectTypeName::Lockable, { ESM::REC_CONT, ESM::REC_DOOR, ESM::REC_CONT4, ESM::REC_DOOR4 })); @@ -223,18 +237,22 @@ namespace MWLua addType(ObjectTypeName::ESM4Clothing, { ESM::REC_CLOT4 }); addType(ObjectTypeName::ESM4Container, { ESM::REC_CONT4 }); addESM4DoorBindings(addType(ObjectTypeName::ESM4Door, { ESM::REC_DOOR4 }, ObjectTypeName::Lockable), context); + addType(ObjectTypeName::ESM4Flora, { ESM::REC_FLOR4 }); addType(ObjectTypeName::ESM4Furniture, { ESM::REC_FURN4 }); addType(ObjectTypeName::ESM4Ingredient, { ESM::REC_INGR4 }); + addType(ObjectTypeName::ESM4ItemMod, { ESM::REC_IMOD4 }); addType(ObjectTypeName::ESM4Light, { ESM::REC_LIGH4 }); addType(ObjectTypeName::ESM4MiscItem, { ESM::REC_MISC4 }); + addType(ObjectTypeName::ESM4MovableStatic, { ESM::REC_MSTT4 }); addType(ObjectTypeName::ESM4Potion, { ESM::REC_ALCH4 }); addType(ObjectTypeName::ESM4Static, { ESM::REC_STAT4 }); + addType(ObjectTypeName::ESM4StaticCollection, { ESM::REC_SCOL4 }); addESM4TerminalBindings(addType(ObjectTypeName::ESM4Terminal, { ESM::REC_TERM4 }), context); addType(ObjectTypeName::ESM4Tree, { ESM::REC_TREE4 }); addType(ObjectTypeName::ESM4Weapon, { ESM::REC_WEAP4 }); - sol::table typeToPackage = getTypeToPackageTable(context.mLua->sol()); - sol::table packageToType = getPackageToTypeTable(context.mLua->sol()); + sol::table typeToPackage = getTypeToPackageTable(lua); + sol::table packageToType = getPackageToTypeTable(lua); for (const auto& [type, name] : luaObjectTypeInfo) { sol::object t = types[name]; @@ -244,6 +262,7 @@ namespace MWLua packageToType[t] = type; } - return LuaUtil::makeReadOnly(types); + lua["openmw_types"] = LuaUtil::makeReadOnly(types); + return lua["openmw_types"]; } } diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp index 8cd126b007..76bd2848e0 100644 --- a/apps/openmw/mwlua/types/types.hpp +++ b/apps/openmw/mwlua/types/types.hpp @@ -6,23 +6,8 @@ #include #include -#include "apps/openmw/mwbase/environment.hpp" -#include "apps/openmw/mwbase/world.hpp" -#include "apps/openmw/mwworld/esmstore.hpp" -#include "apps/openmw/mwworld/store.hpp" - #include "../context.hpp" -#include "../object.hpp" - -namespace sol -{ - // Ensure sol does not try to create the automatic Container or usertype bindings for Store. - // They include write operations and we want the store to be read-only. - template - struct is_automagical> : std::false_type - { - }; -} +#include "../recordstore.hpp" namespace MWLua { @@ -47,7 +32,7 @@ namespace MWLua void addBookBindings(sol::table book, const Context& context); void addContainerBindings(sol::table container, const Context& context); void addDoorBindings(sol::table door, const Context& context); - void addItemBindings(sol::table item); + void addItemBindings(sol::table item, const Context& context); void addActorBindings(sol::table actor, const Context& context); void addWeaponBindings(sol::table weapon, const Context& context); void addNpcBindings(sol::table npc, const Context& context); @@ -69,34 +54,6 @@ namespace MWLua void addESM4DoorBindings(sol::table door, const Context& context); void addESM4TerminalBindings(sol::table term, const Context& context); - - template - void addRecordFunctionBinding( - sol::table& table, const Context& context, const std::string& recordName = std::string(T::getRecordType())) - { - const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); - - table["record"] = sol::overload([](const Object& obj) -> const T* { return obj.ptr().get()->mBase; }, - [&store](std::string_view id) -> const T* { return store.find(ESM::RefId::deserializeText(id)); }); - - // Define a custom user type for the store. - // Provide the interface of a read-only array. - using StoreT = MWWorld::Store; - sol::state_view& lua = context.mLua->sol(); - sol::usertype storeT = lua.new_usertype(recordName + "WorldStore"); - storeT[sol::meta_function::to_string] = [recordName](const StoreT& store) { - return "{" + std::to_string(store.getSize()) + " " + recordName + " records}"; - }; - storeT[sol::meta_function::length] = [](const StoreT& store) { return store.getSize(); }; - storeT[sol::meta_function::index] = [](const StoreT& store, size_t index) -> const T* { - return store.at(index - 1); // Translate from Lua's 1-based indexing. - }; - storeT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); - storeT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); - - // Provide access to the store. - table["records"] = &store; - } } #endif // MWLUA_TYPES_H diff --git a/apps/openmw/mwlua/types/weapon.cpp b/apps/openmw/mwlua/types/weapon.cpp index 1cbe9b1d26..d5c52c8c4f 100644 --- a/apps/openmw/mwlua/types/weapon.cpp +++ b/apps/openmw/mwlua/types/weapon.cpp @@ -1,13 +1,14 @@ #include "types.hpp" +#include "modelproperty.hpp" + #include #include +#include #include #include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" namespace sol { @@ -16,7 +17,6 @@ namespace sol { }; } -#include namespace { @@ -24,37 +24,74 @@ namespace ESM::Weapon tableToWeapon(const sol::table& rec) { ESM::Weapon weapon; - weapon.mName = rec["name"]; - weapon.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - weapon.mIcon = rec["icon"]; - std::string_view enchantId = rec["enchant"].get(); - weapon.mEnchant = ESM::RefId::deserializeText(enchantId); - std::string_view scriptId = rec["mwscript"].get(); - weapon.mScript = ESM::RefId::deserializeText(scriptId); - weapon.mData.mFlags = 0; - weapon.mRecordFlags = 0; - if (rec["isMagical"]) - weapon.mData.mFlags |= ESM::Weapon::Magical; - if (rec["isSilver"]) - weapon.mData.mFlags |= ESM::Weapon::Silver; - int weaponType = rec["type"].get(); - if (weaponType >= 0 && weaponType <= ESM::Weapon::MarksmanThrown) - weapon.mData.mType = weaponType; + if (rec["template"] != sol::nil) + weapon = LuaUtil::cast(rec["template"]); else - throw std::runtime_error("Invalid Weapon Type provided: " + std::to_string(weaponType)); + weapon.blank(); - weapon.mData.mWeight = rec["weight"]; - weapon.mData.mValue = rec["value"]; - weapon.mData.mHealth = rec["health"]; - weapon.mData.mSpeed = rec["speed"]; - weapon.mData.mReach = rec["reach"]; - weapon.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); - weapon.mData.mChop[0] = rec["chopMinDamage"]; - weapon.mData.mChop[1] = rec["chopMaxDamage"]; - weapon.mData.mSlash[0] = rec["slashMinDamage"]; - weapon.mData.mSlash[1] = rec["slashMaxDamage"]; - weapon.mData.mThrust[0] = rec["thrustMinDamage"]; - weapon.mData.mThrust[1] = rec["thrustMaxDamage"]; + if (rec["name"] != sol::nil) + weapon.mName = rec["name"]; + if (rec["model"] != sol::nil) + weapon.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + weapon.mIcon = rec["icon"]; + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + weapon.mEnchant = ESM::RefId::deserializeText(enchantId); + } + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + weapon.mScript = ESM::RefId::deserializeText(scriptId); + } + if (auto isMagical = rec["isMagical"]; isMagical != sol::nil) + { + if (isMagical) + weapon.mData.mFlags |= ESM::Weapon::Magical; + else + weapon.mData.mFlags &= ~ESM::Weapon::Magical; + } + if (auto isSilver = rec["isSilver"]; isSilver != sol::nil) + { + if (isSilver) + weapon.mData.mFlags |= ESM::Weapon::Silver; + else + weapon.mData.mFlags &= ~ESM::Weapon::Silver; + } + + if (rec["type"] != sol::nil) + { + int weaponType = rec["type"].get(); + if (weaponType >= 0 && weaponType <= ESM::Weapon::MarksmanThrown) + weapon.mData.mType = weaponType; + else + throw std::runtime_error("Invalid Weapon Type provided: " + std::to_string(weaponType)); + } + if (rec["weight"] != sol::nil) + weapon.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + weapon.mData.mValue = rec["value"]; + if (rec["health"] != sol::nil) + weapon.mData.mHealth = rec["health"]; + if (rec["speed"] != sol::nil) + weapon.mData.mSpeed = rec["speed"]; + if (rec["reach"] != sol::nil) + weapon.mData.mReach = rec["reach"]; + if (rec["enchantCapacity"] != sol::nil) + weapon.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + if (rec["chopMinDamage"] != sol::nil) + weapon.mData.mChop[0] = rec["chopMinDamage"]; + if (rec["chopMaxDamage"] != sol::nil) + weapon.mData.mChop[1] = rec["chopMaxDamage"]; + if (rec["slashMinDamage"] != sol::nil) + weapon.mData.mSlash[0] = rec["slashMinDamage"]; + if (rec["slashMaxDamage"] != sol::nil) + weapon.mData.mSlash[1] = rec["slashMaxDamage"]; + if (rec["thrustMinDamage"] != sol::nil) + weapon.mData.mThrust[0] = rec["thrustMinDamage"]; + if (rec["thrustMaxDamage"] != sol::nil) + weapon.mData.mThrust[1] = rec["thrustMaxDamage"]; return weapon; } @@ -64,44 +101,44 @@ namespace MWLua { void addWeaponBindings(sol::table weapon, const Context& context) { - weapon["TYPE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "ShortBladeOneHand", ESM::Weapon::ShortBladeOneHand }, - { "LongBladeOneHand", ESM::Weapon::LongBladeOneHand }, - { "LongBladeTwoHand", ESM::Weapon::LongBladeTwoHand }, - { "BluntOneHand", ESM::Weapon::BluntOneHand }, - { "BluntTwoClose", ESM::Weapon::BluntTwoClose }, - { "BluntTwoWide", ESM::Weapon::BluntTwoWide }, - { "SpearTwoWide", ESM::Weapon::SpearTwoWide }, - { "AxeOneHand", ESM::Weapon::AxeOneHand }, - { "AxeTwoHand", ESM::Weapon::AxeTwoHand }, - { "MarksmanBow", ESM::Weapon::MarksmanBow }, - { "MarksmanCrossbow", ESM::Weapon::MarksmanCrossbow }, - { "MarksmanThrown", ESM::Weapon::MarksmanThrown }, - { "Arrow", ESM::Weapon::Arrow }, - { "Bolt", ESM::Weapon::Bolt }, - })); + sol::state_view lua = context.sol(); + weapon["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "ShortBladeOneHand", ESM::Weapon::ShortBladeOneHand }, + { "LongBladeOneHand", ESM::Weapon::LongBladeOneHand }, + { "LongBladeTwoHand", ESM::Weapon::LongBladeTwoHand }, + { "BluntOneHand", ESM::Weapon::BluntOneHand }, + { "BluntTwoClose", ESM::Weapon::BluntTwoClose }, + { "BluntTwoWide", ESM::Weapon::BluntTwoWide }, + { "SpearTwoWide", ESM::Weapon::SpearTwoWide }, + { "AxeOneHand", ESM::Weapon::AxeOneHand }, + { "AxeTwoHand", ESM::Weapon::AxeTwoHand }, + { "MarksmanBow", ESM::Weapon::MarksmanBow }, + { "MarksmanCrossbow", ESM::Weapon::MarksmanCrossbow }, + { "MarksmanThrown", ESM::Weapon::MarksmanThrown }, + { "Arrow", ESM::Weapon::Arrow }, + { "Bolt", ESM::Weapon::Bolt }, + })); auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addRecordFunctionBinding(weapon, context); weapon["createRecordDraft"] = tableToWeapon; - sol::usertype record = context.mLua->sol().new_usertype("ESM3_Weapon"); + sol::usertype record = lua.new_usertype("ESM3_Weapon"); record[sol::meta_function::to_string] = [](const ESM::Weapon& rec) -> std::string { return "ESM3_Weapon[" + rec.mId.toDebugString() + "]"; }; record["id"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mId.serializeText(); }); record["name"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mName; }); - record["model"] = sol::readonly_property([vfs](const ESM::Weapon& rec) -> std::string { - return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); - }); + addModelProperty(record); record["icon"] = sol::readonly_property([vfs](const ESM::Weapon& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); record["enchant"] = sol::readonly_property( - [](const ESM::Weapon& rec) -> std::string { return rec.mEnchant.serializeText(); }); - record["mwscript"] - = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mScript.serializeText(); }); + [](const ESM::Weapon& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mEnchant); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); record["isMagical"] = sol::readonly_property( [](const ESM::Weapon& rec) -> bool { return rec.mData.mFlags & ESM::Weapon::Magical; }); record["isSilver"] = sol::readonly_property( diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index eba761ed97..9652df1238 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -1,5 +1,6 @@ #include "uibindings.hpp" +#include #include #include #include @@ -36,18 +37,7 @@ namespace MWLua } } - // Lua arrays index from 1 - inline size_t fromLuaIndex(size_t i) - { - return i - 1; - } - inline size_t toLuaIndex(size_t i) - { - return i + 1; - } - const std::unordered_map modeToName{ - { MWGui::GM_Settings, "SettingsMenu" }, { MWGui::GM_Inventory, "Interface" }, { MWGui::GM_Container, "Container" }, { MWGui::GM_Companion, "Companion" }, @@ -90,35 +80,41 @@ namespace MWLua }(); } - sol::table initUserInterfacePackage(const Context& context) + sol::table registerUiApi(const Context& context) { + sol::state_view lua = context.sol(); + bool menu = context.mType == Context::Menu; + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - auto element = context.mLua->sol().new_usertype("Element"); - element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; }, - [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); - element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { - if (element->mDestroy || element->mUpdate) - return; - element->mUpdate = true; - luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI"); + sol::table api(lua, sol::create); + api["_setHudVisibility"] = [luaManager = context.mLuaManager](bool state) { + luaManager->addAction([state] { MWBase::Environment::get().getWindowManager()->setHudVisibility(state); }); }; - element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { - if (element->mDestroy) - return; - element->mDestroy = true; - luaManager->addAction([element] { wrapAction(element, [&] { element->destroy(); }); }, "Destroy UI"); - }; - - sol::table api = context.mLua->newTable(); + api["_isHudVisible"] = []() -> bool { return MWBase::Environment::get().getWindowManager()->isHudVisible(); }; api["showMessage"] - = [luaManager = context.mLuaManager](std::string_view message) { luaManager->addUIMessage(message); }; - api["CONSOLE_COLOR"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ - { "Default", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Default.substr(1)) }, - { "Error", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Error.substr(1)) }, - { "Success", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Success.substr(1)) }, - { "Info", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Info.substr(1)) }, - })); + = [luaManager = context.mLuaManager](std::string_view message, const sol::optional& options) { + MWGui::ShowInDialogueMode mode = MWGui::ShowInDialogueMode_IfPossible; + if (options.has_value()) + { + auto showInDialogue = options->get>("showInDialogue"); + if (showInDialogue.has_value()) + { + if (*showInDialogue) + mode = MWGui::ShowInDialogueMode_Only; + else + mode = MWGui::ShowInDialogueMode_Never; + } + } + luaManager->addUIMessage(message, mode); + }; + api["CONSOLE_COLOR"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Default", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Default.substr(1)) }, + { "Error", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Error.substr(1)) }, + { "Success", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Success.substr(1)) }, + { "Info", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Info.substr(1)) }, + })); api["printToConsole"] = [luaManager = context.mLuaManager](const std::string& message, const Misc::Color& color) { luaManager->addInGameConsoleMessage(message + "\n", color); @@ -126,6 +122,7 @@ namespace MWLua api["setConsoleMode"] = [luaManager = context.mLuaManager, windowManager](std::string_view mode) { luaManager->addAction([mode = std::string(mode), windowManager] { windowManager->setConsoleMode(mode); }); }; + api["getConsoleMode"] = [windowManager]() -> std::string_view { return windowManager->getConsoleMode(); }; api["setConsoleSelectedObject"] = [luaManager = context.mLuaManager, windowManager](const sol::object& obj) { if (obj == sol::nil) luaManager->addAction([windowManager] { windowManager->setConsoleSelectedObject(MWWorld::Ptr()); }); @@ -138,38 +135,33 @@ namespace MWLua } }; api["content"] = LuaUi::loadContentConstructor(context.mLua); - api["create"] = [luaManager = context.mLuaManager](const sol::table& layout) { - auto element = LuaUi::Element::make(layout); + + api["create"] = [luaManager = context.mLuaManager, menu](const sol::table& layout) { + auto element = LuaUi::Element::make(layout, menu); luaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI"); return element; }; - api["updateAll"] = [context]() { - LuaUi::Element::forEach([](LuaUi::Element* e) { e->mUpdate = true; }); - context.mLuaManager->addAction( - []() { LuaUi::Element::forEach([](LuaUi::Element* e) { e->update(); }); }, "Update all UI elements"); + + api["updateAll"] = [luaManager = context.mLuaManager, menu]() { + LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { + if (e->mState == LuaUi::Element::Created) + e->mState = LuaUi::Element::Update; + }); + luaManager->addAction([menu]() { LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { e->update(); }); }, + "Update all menu UI elements"); }; api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; }; - auto uiLayer = context.mLua->sol().new_usertype("UiLayer"); - uiLayer["name"] = sol::property([](LuaUi::Layer& self) { return self.name(); }); - uiLayer["size"] = sol::property([](LuaUi::Layer& self) { return self.size(); }); - uiLayer[sol::meta_function::to_string] - = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; - - sol::table layers = context.mLua->newTable(); - layers[sol::meta_function::length] = []() { return LuaUi::Layer::count(); }; - layers[sol::meta_function::index] = [](size_t index) { - index = fromLuaIndex(index); - return LuaUi::Layer(index); - }; - layers["indexOf"] = [](std::string_view name) -> sol::optional { + sol::table layersTable(lua, sol::create); + layersTable["indexOf"] = [](std::string_view name) -> sol::optional { size_t index = LuaUi::Layer::indexOf(name); if (index == LuaUi::Layer::count()) return sol::nullopt; else - return toLuaIndex(index); + return LuaUtil::toLuaIndex(index); }; - layers["insertAfter"] = [context](std::string_view afterName, std::string_view name, const sol::object& opt) { + layersTable["insertAfter"] = [context]( + std::string_view afterName, std::string_view name, const sol::object& opt) { LuaUi::Layer::Options options; options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); size_t index = LuaUi::Layer::indexOf(afterName); @@ -178,7 +170,8 @@ namespace MWLua index++; context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); }; - layers["insertBefore"] = [context](std::string_view beforename, std::string_view name, const sol::object& opt) { + layersTable["insertBefore"] = [context]( + std::string_view beforename, std::string_view name, const sol::object& opt) { LuaUi::Layer::Options options; options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); size_t index = LuaUi::Layer::indexOf(beforename); @@ -186,6 +179,16 @@ namespace MWLua throw std::logic_error(std::string("Layer not found")); context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); }; + sol::table layers = LuaUtil::makeReadOnly(layersTable); + sol::table layersMeta = layers[sol::metatable_key]; + layersMeta[sol::meta_function::length] = []() { return LuaUi::Layer::count(); }; + layersMeta[sol::meta_function::index] = sol::overload( + [](const sol::object& self, size_t index) { + index = LuaUtil::fromLuaIndex(index); + return LuaUi::Layer(index); + }, + [layersTable]( + const sol::object& self, std::string_view key) { return layersTable.raw_get(key); }); { auto pairs = [layers](const sol::object&) { auto next = [](const sol::table& l, size_t i) -> sol::optional> { @@ -196,21 +199,22 @@ namespace MWLua }; return std::make_tuple(next, layers, 0); }; - layers[sol::meta_function::pairs] = pairs; - layers[sol::meta_function::ipairs] = pairs; + layersMeta[sol::meta_function::pairs] = pairs; + layersMeta[sol::meta_function::ipairs] = pairs; } - api["layers"] = LuaUtil::makeReadOnly(layers); + api["layers"] = layers; - sol::table typeTable = context.mLua->newTable(); + sol::table typeTable(lua, sol::create); for (const auto& it : LuaUi::widgetTypeToName()) typeTable.set(it.second, it.first); api["TYPE"] = LuaUtil::makeStrictReadOnly(typeTable); - api["ALIGNMENT"] = LuaUtil::makeStrictReadOnly( - context.mLua->tableFromPairs({ { "Start", LuaUi::Alignment::Start }, - { "Center", LuaUi::Alignment::Center }, { "End", LuaUi::Alignment::End } })); + api["ALIGNMENT"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { { "Start", LuaUi::Alignment::Start }, { "Center", LuaUi::Alignment::Center }, + { "End", LuaUi::Alignment::End } })); api["registerSettingsPage"] = &LuaUi::registerSettingsPage; + api["removeSettingsPage"] = &LuaUi::removeSettingsPage; api["texture"] = [luaManager = context.mLuaManager](const sol::table& options) { LuaUi::TextureData data; @@ -225,13 +229,10 @@ namespace MWLua sol::object size = LuaUtil::getFieldOrNil(options, "size"); if (size.is()) data.mSize = size.as(); - return luaManager->uiResourceManager()->registerTexture(data); + return luaManager->uiResourceManager()->registerTexture(std::move(data)); }; - api["screenSize"] = []() { - return osg::Vec2f( - Settings::Manager::getInt("resolution x", "Video"), Settings::Manager::getInt("resolution y", "Video")); - }; + api["screenSize"] = []() { return osg::Vec2f(Settings::video().mResolutionX, Settings::video().mResolutionY); }; api["_getAllUiModes"] = [](sol::this_state lua) { sol::table res(lua, sol::create); @@ -250,9 +251,9 @@ namespace MWLua = [windowManager, luaManager = context.mLuaManager](sol::table modes, sol::optional arg) { std::vector newStack(modes.size()); for (unsigned i = 0; i < newStack.size(); ++i) - newStack[i] = nameToMode.at(LuaUtil::cast(modes[i + 1])); + newStack[i] = nameToMode.at(LuaUtil::cast(modes[LuaUtil::toLuaIndex(i)])); luaManager->addAction( - [windowManager, newStack, arg]() { + [windowManager, newStack = std::move(newStack), arg = std::move(arg)]() { MWWorld::Ptr ptr; if (arg.has_value()) ptr = arg->ptr(); @@ -263,7 +264,7 @@ namespace MWLua // TODO: Maybe disallow opening/closing special modes (main menu, settings, loading screen) // from player scripts. Add new Lua context "menu" that can do it. for (unsigned i = stack.size() - common; i > 0; i--) - windowManager->popGuiMode(); + windowManager->popGuiMode(true); if (common == newStack.size() && !newStack.empty() && arg.has_value()) windowManager->pushGuiMode(newStack.back(), ptr); for (unsigned i = common; i < newStack.size(); ++i) @@ -289,9 +290,54 @@ namespace MWLua }; // TODO - // api["_showHUD"] = [](bool) {}; // api["_showMouseCursor"] = [](bool) {}; - return LuaUtil::makeReadOnly(api); + return api; + } + + sol::table initUserInterfacePackage(const Context& context) + { + if (context.initializeOnce("openmw_ui_usertypes")) + { + auto element = context.sol().new_usertype("UiElement"); + element[sol::meta_function::to_string] = [](const LuaUi::Element& element) { + std::stringstream res; + res << "UiElement"; + if (element.mLayer != "") + res << "[" << element.mLayer << "]"; + return res.str(); + }; + element["layout"] = sol::property([](const LuaUi::Element& element) { return element.mLayout; }, + [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); + element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { + if (element->mState != LuaUi::Element::Created) + return; + element->mState = LuaUi::Element::Update; + luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI"); + }; + element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { + if (element->mState == LuaUi::Element::Destroyed) + return; + element->mState = LuaUi::Element::Destroy; + luaManager->addAction( + [element] { wrapAction(element, [&] { LuaUi::Element::erase(element.get()); }); }, "Destroy UI"); + }; + + auto uiLayer = context.sol().new_usertype("UiLayer"); + uiLayer["name"] + = sol::readonly_property([](LuaUi::Layer& self) -> std::string_view { return self.name(); }); + uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); }); + uiLayer[sol::meta_function::to_string] + = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; + } + + sol::object cached = context.getTypePackage("openmw_ui"); + if (cached != sol::nil) + return cached; + else + { + sol::table api = LuaUtil::makeReadOnly(registerUiApi(context)); + return context.setTypePackage(api, "openmw_ui"); + } } } diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp index 6565ee0bde..478f725d7c 100644 --- a/apps/openmw/mwlua/userdataserializer.cpp +++ b/apps/openmw/mwlua/userdataserializer.cpp @@ -52,7 +52,7 @@ namespace MWLua { std::vector buf; buf.reserve(objList->size()); - for (const ESM::RefNum& v : *objList) + for (ESM::RefNum v : *objList) buf.push_back({ Misc::toLittleEndian(v.mIndex), Misc::toLittleEndian(v.mContentFile) }); append(out, sObjListTypeName, buf.data(), buf.size() * sizeof(ESM::RefNum)); } diff --git a/apps/openmw/mwlua/vfsbindings.cpp b/apps/openmw/mwlua/vfsbindings.cpp new file mode 100644 index 0000000000..3186db26ca --- /dev/null +++ b/apps/openmw/mwlua/vfsbindings.cpp @@ -0,0 +1,355 @@ +#include "vfsbindings.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "context.hpp" + +namespace MWLua +{ + namespace + { + // Too many arguments may cause stack corruption and crash. + constexpr std::size_t sMaximumReadArguments = 20; + + // Print a message if we read a large chunk of file to string. + constexpr std::size_t sFileSizeWarningThreshold = 1024 * 1024; + + struct FileHandle + { + public: + FileHandle(Files::IStreamPtr stream, std::string_view fileName) + { + mFilePtr = std::move(stream); + mFileName = fileName; + } + + Files::IStreamPtr mFilePtr; + std::string mFileName; + }; + + std::ios_base::seekdir getSeekDir(FileHandle& self, std::string_view whence) + { + if (whence == "cur") + return std::ios_base::cur; + if (whence == "set") + return std::ios_base::beg; + if (whence == "end") + return std::ios_base::end; + + throw std::runtime_error( + "Error when handling '" + self.mFileName + "': invalid seek direction: '" + std::string(whence) + "'."); + } + + size_t getBytesLeftInStream(Files::IStreamPtr& file) + { + auto oldPos = file->tellg(); + file->seekg(0, std::ios_base::end); + auto newPos = file->tellg(); + file->seekg(oldPos, std::ios_base::beg); + + return newPos - oldPos; + } + + void printLargeDataMessage(FileHandle& file, size_t size) + { + if (!file.mFilePtr || !Settings::lua().mLuaDebug || size < sFileSizeWarningThreshold) + return; + + Log(Debug::Verbose) << "Read a large data chunk (" << size << " bytes) from '" << file.mFileName << "'."; + } + + sol::object readFile(sol::this_state lua, FileHandle& file) + { + std::ostringstream os; + if (file.mFilePtr && file.mFilePtr->peek() != EOF) + os << file.mFilePtr->rdbuf(); + + auto result = os.str(); + printLargeDataMessage(file, result.size()); + return sol::make_object(lua, std::move(result)); + } + + sol::object readLineFromFile(sol::this_state lua, FileHandle& file) + { + std::string result; + if (file.mFilePtr && std::getline(*file.mFilePtr, result)) + { + printLargeDataMessage(file, result.size()); + return sol::make_object(lua, result); + } + + return sol::nil; + } + + sol::object readNumberFromFile(sol::this_state lua, Files::IStreamPtr& file) + { + double number = 0; + if (file && *file >> number) + return sol::make_object(lua, number); + + return sol::nil; + } + + sol::object readCharactersFromFile(sol::this_state lua, FileHandle& file, size_t count) + { + if (count <= 0 && file.mFilePtr->peek() != EOF) + return sol::make_object(lua, std::string()); + + auto bytesLeft = getBytesLeftInStream(file.mFilePtr); + if (bytesLeft <= 0) + return sol::nil; + + if (count > bytesLeft) + count = bytesLeft; + + std::string result(count, '\0'); + if (file.mFilePtr->read(&result[0], count)) + { + printLargeDataMessage(file, result.size()); + return sol::make_object(lua, result); + } + + return sol::nil; + } + + void validateFile(const FileHandle& self) + { + if (self.mFilePtr) + return; + + throw std::runtime_error("Error when handling '" + self.mFileName + "': attempt to use a closed file."); + } + + sol::variadic_results seek( + sol::this_state lua, FileHandle& self, std::ios_base::seekdir dir, std::streamoff off) + { + sol::variadic_results values; + try + { + self.mFilePtr->seekg(off, dir); + if (self.mFilePtr->fail() || self.mFilePtr->bad()) + { + auto msg = "Failed to seek in file '" + self.mFileName + "'"; + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + else + { + // tellg returns std::streampos which is not required to be a numeric type. It is required to be + // convertible to std::streamoff which is a number + values.push_back(sol::make_object(lua, self.mFilePtr->tellg())); + } + } + catch (std::exception& e) + { + auto msg = "Failed to seek in file '" + self.mFileName + "': " + std::string(e.what()); + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + } + } + + sol::table initVFSPackage(const Context& context) + { + sol::table api(context.mLua->unsafeState(), sol::create); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + sol::usertype handle = context.sol().new_usertype("FileHandle"); + handle["fileName"] + = sol::readonly_property([](const FileHandle& self) -> std::string_view { return self.mFileName; }); + handle[sol::meta_function::to_string] = [](const FileHandle& self) { + return "FileHandle{'" + self.mFileName + "'" + (!self.mFilePtr ? ", closed" : "") + "}"; + }; + handle["seek"] = sol::overload( + [](sol::this_state lua, FileHandle& self, std::string_view whence, sol::optional offset) { + validateFile(self); + + auto off = static_cast(offset.value_or(0)); + auto dir = getSeekDir(self, whence); + + return seek(lua, self, dir, off); + }, + [](sol::this_state lua, FileHandle& self, sol::optional offset) { + validateFile(self); + + auto off = static_cast(offset.value_or(0)); + + return seek(lua, self, std::ios_base::cur, off); + }); + handle["lines"] = [](sol::this_state lua, FileHandle& self) { + return sol::as_function([&lua, &self]() mutable { + validateFile(self); + return readLineFromFile(lua, self); + }); + }; + + api["lines"] = [vfs](sol::this_state lua, std::string_view fileName) { + auto normalizedName = VFS::Path::normalizeFilename(fileName); + return sol::as_function( + [lua, file = FileHandle(vfs->getNormalized(normalizedName), normalizedName)]() mutable { + validateFile(file); + auto result = readLineFromFile(lua, file); + if (result == sol::nil) + file.mFilePtr.reset(); + + return result; + }); + }; + + handle["close"] = [](sol::this_state lua, FileHandle& self) { + sol::variadic_results values; + try + { + self.mFilePtr.reset(); + if (self.mFilePtr) + { + auto msg = "Can not close file '" + self.mFileName + "': file handle is still opened."; + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + else + values.push_back(sol::make_object(lua, true)); + } + catch (std::exception& e) + { + auto msg = "Can not close file '" + self.mFileName + "': " + std::string(e.what()); + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + }; + + handle["read"] = [](sol::this_state lua, FileHandle& self, const sol::variadic_args args) { + validateFile(self); + + if (args.size() > sMaximumReadArguments) + throw std::runtime_error( + "Error when handling '" + self.mFileName + "': too many arguments for 'read'."); + + sol::variadic_results values; + // If there are no arguments, read a string + if (args.size() == 0) + { + values.push_back(readLineFromFile(lua, self)); + return values; + } + + bool success = true; + size_t i = 0; + for (i = 0; i < args.size() && success; i++) + { + if (args[i].is()) + { + auto format = args[i].as(); + + if (format == "*a" || format == "*all") + { + values.push_back(readFile(lua, self)); + continue; + } + + if (format == "*n" || format == "*number") + { + auto result = readNumberFromFile(lua, self.mFilePtr); + values.push_back(result); + if (result == sol::nil) + success = false; + continue; + } + + if (format == "*l" || format == "*line") + { + auto result = readLineFromFile(lua, self); + values.push_back(result); + if (result == sol::nil) + success = false; + continue; + } + + throw std::runtime_error("Error when handling '" + self.mFileName + "': bad argument #" + + std::to_string(i + 1) + " to 'read' (invalid format)"); + } + else if (args[i].is()) + { + int number = args[i].as(); + auto result = readCharactersFromFile(lua, self, number); + values.push_back(result); + if (result == sol::nil) + success = false; + } + } + + // We should return nil if we just reached the end of stream + if (!success && self.mFilePtr->eof()) + return values; + + if (!success && (self.mFilePtr->fail() || self.mFilePtr->bad())) + { + auto msg = "Error when handling '" + self.mFileName + "': can not read data for argument #" + + std::to_string(i); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + }; + + api["open"] = [vfs](sol::this_state lua, std::string_view fileName) { + sol::variadic_results values; + try + { + auto normalizedName = VFS::Path::normalizeFilename(fileName); + auto handle = FileHandle(vfs->getNormalized(normalizedName), normalizedName); + values.push_back(sol::make_object(lua, std::move(handle))); + } + catch (std::exception& e) + { + auto msg = "Can not open file: " + std::string(e.what()); + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + }; + + api["type"] = sol::overload( + [](const FileHandle& handle) -> std::string { + if (handle.mFilePtr) + return "file"; + + return "closed file"; + }, + [](const sol::object&) -> sol::object { return sol::nil; }); + + api["fileExists"] + = [vfs](std::string_view fileName) -> bool { return vfs->exists(VFS::Path::Normalized(fileName)); }; + api["pathsWithPrefix"] = [vfs](std::string_view prefix) { + auto iterator = vfs->getRecursiveDirectoryIterator(prefix); + return sol::as_function([iterator, current = iterator.begin()]() mutable -> sol::optional { + if (current != iterator.end()) + { + const std::string& result = *current; + ++current; + return result; + } + + return sol::nullopt; + }); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/vfsbindings.hpp b/apps/openmw/mwlua/vfsbindings.hpp new file mode 100644 index 0000000000..b251db6fd4 --- /dev/null +++ b/apps/openmw/mwlua/vfsbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_VFSBINDINGS_H +#define MWLUA_VFSBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initVFSPackage(const Context&); +} + +#endif // MWLUA_VFSBINDINGS_H diff --git a/apps/openmw/mwlua/worker.cpp b/apps/openmw/mwlua/worker.cpp index e8b06cf210..5639cc89ed 100644 --- a/apps/openmw/mwlua/worker.cpp +++ b/apps/openmw/mwlua/worker.cpp @@ -2,18 +2,17 @@ #include "luamanagerimp.hpp" -#include +#include "apps/openmw/profile.hpp" #include #include -#include +#include namespace MWLua { - Worker::Worker(LuaManager& manager, osgViewer::Viewer& viewer) + Worker::Worker(LuaManager& manager) : mManager(manager) - , mViewer(viewer) { if (Settings::lua().mLuaNumThreads > 0) mThread = std::thread([this] { run(); }); @@ -29,26 +28,26 @@ namespace MWLua } } - void Worker::allowUpdate() + void Worker::allowUpdate(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats) { if (!mThread) return; { std::lock_guard lk(mMutex); - mUpdateRequest = true; + mUpdateRequest = UpdateRequest{ .mFrameStart = frameStart, .mFrameNumber = frameNumber, .mStats = &stats }; } mCV.notify_one(); } - void Worker::finishUpdate() + void Worker::finishUpdate(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats) { if (mThread) { std::unique_lock lk(mMutex); - mCV.wait(lk, [&] { return !mUpdateRequest; }); + mCV.wait(lk, [&] { return !mUpdateRequest.has_value(); }); } else - update(); + update(frameStart, frameNumber, stats); } void Worker::join() @@ -64,12 +63,10 @@ namespace MWLua } } - void Worker::update() + void Worker::update(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats) { - const osg::Timer_t frameStart = mViewer.getStartTick(); - const unsigned int frameNumber = mViewer.getFrameStamp()->getFrameNumber(); - OMW::ScopedProfile profile( - frameStart, frameNumber, *osg::Timer::instance(), *mViewer.getViewerStats()); + const osg::Timer* const timer = osg::Timer::instance(); + OMW::ScopedProfile profile(frameStart, frameNumber, *timer, stats); mManager.update(); } @@ -79,20 +76,22 @@ namespace MWLua while (true) { std::unique_lock lk(mMutex); - mCV.wait(lk, [&] { return mUpdateRequest || mJoinRequest; }); + mCV.wait(lk, [&] { return mUpdateRequest.has_value() || mJoinRequest; }); if (mJoinRequest) break; + assert(mUpdateRequest.has_value()); + try { - update(); + update(mUpdateRequest->mFrameStart, mUpdateRequest->mFrameNumber, *mUpdateRequest->mStats); } - catch (std::exception& e) + catch (const std::exception& e) { Log(Debug::Error) << "Failed to update LuaManager: " << e.what(); } - mUpdateRequest = false; + mUpdateRequest.reset(); lk.unlock(); mCV.notify_one(); } diff --git a/apps/openmw/mwlua/worker.hpp b/apps/openmw/mwlua/worker.hpp index fed625e1f1..58d69afe71 100644 --- a/apps/openmw/mwlua/worker.hpp +++ b/apps/openmw/mwlua/worker.hpp @@ -1,14 +1,17 @@ #ifndef OPENMW_MWLUA_WORKER_H #define OPENMW_MWLUA_WORKER_H +#include +#include + #include #include #include #include -namespace osgViewer +namespace osg { - class Viewer; + class Stats; } namespace MWLua @@ -18,26 +21,32 @@ namespace MWLua class Worker { public: - explicit Worker(LuaManager& manager, osgViewer::Viewer& viewer); + explicit Worker(LuaManager& manager); ~Worker(); - void allowUpdate(); + void allowUpdate(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - void finishUpdate(); + void finishUpdate(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); void join(); private: - void update(); + struct UpdateRequest + { + osg::Timer_t mFrameStart; + unsigned mFrameNumber; + osg::ref_ptr mStats; + }; + + void update(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats); void run() noexcept; LuaManager& mManager; - osgViewer::Viewer& mViewer; std::mutex mMutex; std::condition_variable mCV; - bool mUpdateRequest = false; + std::optional mUpdateRequest; bool mJoinRequest = false; std::optional mThread; }; diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp new file mode 100644 index 0000000000..ac7bd307cf --- /dev/null +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -0,0 +1,228 @@ +#include "worldbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/action.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "luamanagerimp.hpp" + +#include "animationbindings.hpp" +#include "corebindings.hpp" +#include "mwscriptbindings.hpp" + +namespace MWLua +{ + struct CellsStore + { + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + static void checkGameInitialized(LuaUtil::LuaState* lua) + { + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + throw std::runtime_error( + "This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback()); + } + + static void addWorldTimeBindings(sol::table& api, const Context& context) + { + MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); + + api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); }; + api["setSimulationTimeScale"] = [context, timeManager](float scale) { + context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); }); + }; + + api["pause"] + = [timeManager](sol::optional tag) { timeManager->pause(tag.value_or("paused")); }; + api["unpause"] + = [timeManager](sol::optional tag) { timeManager->unpause(tag.value_or("paused")); }; + api["getPausedTags"] = [timeManager](sol::this_state lua) { + sol::table res(lua, sol::create); + for (const std::string& tag : timeManager->getPausedTags()) + res[tag] = tag; + return res; + }; + } + + static void addCellGetters(sol::table& api, const Context& context) + { + api["getCellByName"] = [](std::string_view name) { + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) }; + }; + api["getCellById"] = [](std::string_view stringId) { + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + ESM::RefId::deserializeText(stringId), /*forceLoad=*/false) }; + }; + api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) { + ESM::RefId worldspace; + if (cellOrName.is()) + worldspace = cellOrName.as().mStore->getCell()->getWorldSpace(); + else if (cellOrName.is() && !cellOrName.as().empty()) + worldspace = MWBase::Environment::get() + .getWorldModel() + ->getCell(cellOrName.as()) + .getCell() + ->getWorldSpace(); + else + worldspace = ESM::Cell::sDefaultWorldspaceId; + return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior( + ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) }; + }; + + const MWWorld::Store* cells3Store = &MWBase::Environment::get().getESMStore()->get(); + const MWWorld::Store* cells4Store = &MWBase::Environment::get().getESMStore()->get(); + auto view = context.sol(); + sol::usertype cells = view.new_usertype("Cells"); + cells[sol::meta_function::length] + = [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); }; + cells[sol::meta_function::index] + = [cells3Store, cells4Store](const CellsStore&, size_t index) -> sol::optional { + if (index > cells3Store->getSize() + cells3Store->getSize() || index == 0) + return sol::nullopt; + + index--; // Translate from Lua's 1-based indexing. + if (index < cells3Store->getSize()) + { + const ESM::Cell* cellRecord = cells3Store->at(index); + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + cellRecord->mId, /*forceLoad=*/false) }; + } + else + { + const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize()); + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + cellRecord->mId, /*forceLoad=*/false) }; + } + }; + cells[sol::meta_function::pairs] = view["ipairsForArray"].template get(); + cells[sol::meta_function::ipairs] = view["ipairsForArray"].template get(); + api["cells"] = CellsStore{}; + } + + sol::table initWorldPackage(const Context& context) + { + sol::table api(context.mLua->unsafeState(), sol::create); + + addCoreTimeBindings(api, context); + addWorldTimeBindings(api, context); + addCellGetters(api, context); + api["mwscript"] = initMWScriptBindings(context); + + ObjectLists* objectLists = context.mObjectLists; + api["activeActors"] = GObjectList{ objectLists->getActorsInScene() }; + api["players"] = GObjectList{ objectLists->getPlayers() }; + + api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional count) -> GObject { + checkGameInitialized(lua); + MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId)); + const MWWorld::Ptr& ptr = mref.getPtr(); + ptr.getRefData().disable(); + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell(); + MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); + return GObject(newPtr); + }; + api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { + ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); + if (!refId.is()) + throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); + return GObject(*refId.getIf()); + }; + + // Creates a new record in the world database. + api["createRecord"] = sol::overload( + [lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(activator); + }, + [lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(armor); + }, + [lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(clothing); + }, + [lua = context.mLua](const ESM::Book& book) -> const ESM::Book* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(book); + }, + [lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(misc); + }, + [lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(potion); + }, + [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(weapon); + }, + [lua = context.mLua](const ESM::Light& light) -> const ESM::Light* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(light); + }); + + api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) { + if (!object.ptr().getRefData().activate()) + return; + context.mLuaManager->addAction( + [object, actor] { + const MWWorld::Ptr& objPtr = object.ptr(); + const MWWorld::Ptr& actorPtr = actor.ptr(); + objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr); + }, + "_runStandardActivationAction"); + }; + api["_runStandardUseAction"] = [context](const GObject& object, const GObject& actor, bool force) { + context.mLuaManager->addAction( + [object, actor, force] { + const MWWorld::Ptr& actorPtr = actor.ptr(); + const MWWorld::Ptr& objectPtr = object.ptr(); + if (actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->useItem(objectPtr, force); + else + { + std::unique_ptr action = objectPtr.getClass().use(objectPtr, force); + action->execute(actorPtr, true); + } + }, + "_runStandardUseAction"); + }; + + api["vfx"] = initWorldVfxBindings(context); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/worldbindings.hpp b/apps/openmw/mwlua/worldbindings.hpp new file mode 100644 index 0000000000..4bd2318b68 --- /dev/null +++ b/apps/openmw/mwlua/worldbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_WORLDBINDINGS_H +#define MWLUA_WORLDBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initWorldPackage(const Context&); +} + +#endif // MWLUA_WORLDBINDINGS_H diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index decbae765b..3cee16f29f 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -49,16 +50,17 @@ namespace void addEffects( std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) { - int currentEffectIndex = 0; for (const auto& enam : list.mList) { + if (enam.mData.mRange != ESM::RT_Self) + continue; ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; - effect.mEffectIndex = currentEffectIndex++; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mEffectIndex = enam.mIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; if (ignoreResistances) effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; @@ -82,12 +84,13 @@ namespace MWMechanics mActiveSpells.mIterating = false; } - ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) - : mId(cast.mId) - , mDisplayName(cast.mSourceName) + ActiveSpells::ActiveSpellParams::ActiveSpellParams( + const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item) + : mSourceSpellId(id) + , mDisplayName(sourceName) , mCasterActorId(-1) - , mItem(cast.mItem) - , mType(cast.mType) + , mItem(item) + , mFlags() , mWorsenings(-1) { if (!caster.isEmpty() && caster.getClass().isActor()) @@ -96,48 +99,52 @@ namespace MWMechanics ActiveSpells::ActiveSpellParams::ActiveSpellParams( const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) - : mId(spell->mId) + : mSourceSpellId(spell->mId) , mDisplayName(spell->mName) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) - , mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability - : ESM::ActiveSpells::Type_Permanent) + , mFlags() , mWorsenings(-1) { assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + setFlag(ESM::ActiveSpells::Flag_SpellStore); + if (spell->mData.mType == ESM::Spell::ST_Ability) + setFlag(ESM::ActiveSpells::Flag_AffectsBaseValues); addEffects(mEffects, spell->mEffects, ignoreResistances); } ActiveSpells::ActiveSpellParams::ActiveSpellParams( const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, const MWWorld::Ptr& actor) - : mId(item.getCellRef().getRefId()) + : mSourceSpellId(item.getCellRef().getRefId()) , mDisplayName(item.getClass().getName(item)) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mItem(item.getCellRef().getRefNum()) - , mType(ESM::ActiveSpells::Type_Enchantment) + , mFlags() , mWorsenings(-1) { assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); addEffects(mEffects, enchantment->mEffects); + setFlag(ESM::ActiveSpells::Flag_Equipment); } ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) - : mId(params.mId) + : mActiveSpellId(params.mActiveSpellId) + , mSourceSpellId(params.mSourceSpellId) , mEffects(params.mEffects) , mDisplayName(params.mDisplayName) , mCasterActorId(params.mCasterActorId) , mItem(params.mItem) - , mType(params.mType) + , mFlags(params.mFlags) , mWorsenings(params.mWorsenings) , mNextWorsening({ params.mNextWorsening }) { } ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) - : mId(params.mId) + : mSourceSpellId(params.mSourceSpellId) , mDisplayName(params.mDisplayName) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mItem(params.mItem) - , mType(params.mType) + , mFlags(params.mFlags) , mWorsenings(-1) { } @@ -145,17 +152,23 @@ namespace MWMechanics ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { ESM::ActiveSpells::ActiveSpellParams params; - params.mId = mId; + params.mActiveSpellId = mActiveSpellId; + params.mSourceSpellId = mSourceSpellId; params.mEffects = mEffects; params.mDisplayName = mDisplayName; params.mCasterActorId = mCasterActorId; params.mItem = mItem; - params.mType = mType; + params.mFlags = mFlags; params.mWorsenings = mWorsenings; params.mNextWorsening = mNextWorsening.toEsm(); return params; } + void ActiveSpells::ActiveSpellParams::setFlag(ESM::ActiveSpells::Flags flag) + { + mFlags = static_cast(mFlags | flag); + } + void ActiveSpells::ActiveSpellParams::worsen() { ++mWorsenings; @@ -174,6 +187,35 @@ namespace MWMechanics mWorsenings = -1; } + ESM::RefId ActiveSpells::ActiveSpellParams::getEnchantment() const + { + // Enchantment id is not stored directly. Instead the enchanted item is stored. + const auto& store = MWBase::Environment::get().getESMStore(); + switch (store->find(mSourceSpellId)) + { + case ESM::REC_ARMO: + return store->get().find(mSourceSpellId)->mEnchant; + case ESM::REC_BOOK: + return store->get().find(mSourceSpellId)->mEnchant; + case ESM::REC_CLOT: + return store->get().find(mSourceSpellId)->mEnchant; + case ESM::REC_WEAP: + return store->get().find(mSourceSpellId)->mEnchant; + default: + return {}; + } + } + + const ESM::Spell* ActiveSpells::ActiveSpellParams::getSpell() const + { + return MWBase::Environment::get().getESMStore()->get().search(getSourceSpellId()); + } + + bool ActiveSpells::ActiveSpellParams::hasFlag(ESM::ActiveSpells::Flags flags) const + { + return static_cast(mFlags & flags) == flags; + } + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) { if (mIterating) @@ -184,8 +226,7 @@ namespace MWMechanics // Erase no longer active spells and effects for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - if (spellIt->mType != ESM::ActiveSpells::Type_Temporary - && spellIt->mType != ESM::ActiveSpells::Type_Consumable) + if (!spellIt->hasFlag(ESM::ActiveSpells::Flag_Temporary)) { ++spellIt; continue; @@ -225,9 +266,13 @@ namespace MWMechanics { if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) + { mSpells.emplace_back(ActiveSpellParams{ spell, ptr }); + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + } } + bool updateSpellWindow = false; if (ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { @@ -250,8 +295,8 @@ namespace MWMechanics if (std::find_if(mSpells.begin(), mSpells.end(), [&](const ActiveSpellParams& params) { return params.mItem == slot->getCellRef().getRefNum() - && params.mType == ESM::ActiveSpells::Type_Enchantment - && params.mId == slot->getCellRef().getRefId(); + && params.hasFlag(ESM::ActiveSpells::Flag_Equipment) + && params.mSourceSpellId == slot->getCellRef().getRefId(); }) != mSpells.end()) continue; @@ -259,11 +304,12 @@ namespace MWMechanics // invisibility manually purgeEffect(ptr, ESM::MagicEffect::Invisibility); applyPurges(ptr); - const ActiveSpellParams& params - = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); + ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); + params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); for (const auto& effect : params.mEffects) MWMechanics::playEffects( ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); + updateSpellWindow = true; } } } @@ -322,9 +368,10 @@ namespace MWMechanics MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation && !reflectStatic->mModel.empty()) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel, vfs), - ESM::MagicEffect::Reflect, false); + const VFS::Path::Normalized reflectStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); + animation->addEffect( + reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); } caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); } @@ -332,12 +379,11 @@ namespace MWMechanics continue; bool remove = false; - if (spellIt->mType == ESM::ActiveSpells::Type_Ability - || spellIt->mType == ESM::ActiveSpells::Type_Permanent) + if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) { try { - remove = !spells.hasSpell(spellIt->mId); + remove = !spells.hasSpell(spellIt->mSourceSpellId); } catch (const std::runtime_error& e) { @@ -345,9 +391,9 @@ namespace MWMechanics Log(Debug::Error) << "Removing active effect: " << e.what(); } } - else if (spellIt->mType == ESM::ActiveSpells::Type_Enchantment) + else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) { - // Remove constant effect enchantments that have been unequipped + // Remove effects tied to equipment that has been unequipped const auto& store = ptr.getClass().getInventoryStore(ptr); remove = true; for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) @@ -368,6 +414,7 @@ namespace MWMechanics for (const auto& effect : params.mEffects) onMagicEffectRemoved(ptr, params, effect); applyPurges(ptr, &spellIt); + updateSpellWindow = true; continue; } ++spellIt; @@ -380,15 +427,23 @@ namespace MWMechanics if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f) creatureStats.getAiSequence().stopCombat(); } + + if (ptr == player && updateSpellWindow) + { + // Something happened with the spell list -- possibly while the game is paused, + // so we want to make the spell window get the memo. + // We don't normally want to do this, so this targets constant enchantments. + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + } } void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - if (spell.mType != ESM::ActiveSpells::Type_Consumable) + if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { auto found = std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& existing) { - return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId - && spell.mItem == existing.mItem; + return spell.mSourceSpellId == existing.mSourceSpellId + && spell.mCasterActorId == existing.mCasterActorId && spell.mItem == existing.mItem; }); if (found != mSpells.end()) { @@ -401,6 +456,7 @@ namespace MWMechanics } } mSpells.emplace_back(spell); + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); } ActiveSpells::ActiveSpells() @@ -418,10 +474,19 @@ namespace MWMechanics return mSpells.end(); } + ActiveSpells::TIterator ActiveSpells::getActiveSpellById(const ESM::RefId& id) + { + for (TIterator it = begin(); it != end(); it++) + if (it->getActiveSpellId() == id) + return it; + return end(); + } + bool ActiveSpells::isSpellActive(const ESM::RefId& id) const { - return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { return spell.mId == id; }) - != mSpells.end(); + return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { + return spell.mSourceSpellId == id; + }) != mSpells.end(); } bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const @@ -430,21 +495,8 @@ namespace MWMechanics if (store->get().search(id) == nullptr) return false; - // Enchantment id is not stored directly. Instead the enchanted item is stored. return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { - switch (store->find(spell.mId)) - { - case ESM::REC_ARMO: - return store->get().find(spell.mId)->mEnchant == id; - case ESM::REC_BOOK: - return store->get().find(spell.mId)->mEnchant == id; - case ESM::REC_CLOT: - return store->get().find(spell.mId)->mEnchant == id; - case ESM::REC_WEAP: - return store->get().find(spell.mId)->mEnchant == id; - default: - return false; - } + return spell.getEnchantment() == id; }) != mSpells.end(); } @@ -543,9 +595,14 @@ namespace MWMechanics return removedCurrentSpell; } - void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const ESM::RefId& id) + void ActiveSpells::removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) { - purge([=](const ActiveSpellParams& params) { return params.mId == id; }, ptr); + purge([=](const ActiveSpellParams& params) { return params.mSourceSpellId == id; }, ptr); + } + + void ActiveSpells::removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) + { + purge([=](const ActiveSpellParams& params) { return params.mActiveSpellId == id; }, ptr); } void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg) @@ -590,19 +647,19 @@ namespace MWMechanics void ActiveSpells::readState(const ESM::ActiveSpells& state) { for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) + { mSpells.emplace_back(ActiveSpellParams{ spell }); + // Generate ID for older saves that didn't have any. + if (mSpells.back().getActiveSpellId().empty()) + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + } for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) mQueue.emplace_back(ActiveSpellParams{ spell }); } void ActiveSpells::unloadActor(const MWWorld::Ptr& ptr) { - purge( - [](const auto& spell) { - return spell.getType() == ESM::ActiveSpells::Type_Consumable - || spell.getType() == ESM::ActiveSpells::Type_Temporary; - }, - ptr); + purge([](const auto& spell) { return spell.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, ptr); mQueue.clear(); } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 87497d9d7a..e4fa60ddb6 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -33,12 +33,13 @@ namespace MWMechanics using ActiveEffect = ESM::ActiveEffect; class ActiveSpellParams { - ESM::RefId mId; + ESM::RefId mActiveSpellId; + ESM::RefId mSourceSpellId; std::vector mEffects; std::string mDisplayName; int mCasterActorId; ESM::RefNum mItem; - ESM::ActiveSpells::EffectType mType; + ESM::ActiveSpells::Flags mFlags; int mWorsenings; MWWorld::TimeStamp mNextWorsening; MWWorld::Ptr mSource; @@ -57,15 +58,17 @@ namespace MWMechanics friend class ActiveSpells; public: - ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster); + ActiveSpellParams( + const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item); - const ESM::RefId& getId() const { return mId; } + ESM::RefId getActiveSpellId() const { return mActiveSpellId; } + void setActiveSpellId(ESM::RefId id) { mActiveSpellId = id; } + + const ESM::RefId& getSourceSpellId() const { return mSourceSpellId; } const std::vector& getEffects() const { return mEffects; } std::vector& getEffects() { return mEffects; } - ESM::ActiveSpells::EffectType getType() const { return mType; } - int getCasterActorId() const { return mCasterActorId; } int getWorsenings() const { return mWorsenings; } @@ -73,6 +76,11 @@ namespace MWMechanics const std::string& getDisplayName() const { return mDisplayName; } ESM::RefNum getItem() const { return mItem; } + ESM::RefId getEnchantment() const; + + const ESM::Spell* getSpell() const; + bool hasFlag(ESM::ActiveSpells::Flags flags) const; + void setFlag(ESM::ActiveSpells::Flags flags); // Increments worsenings count and sets the next timestamp void worsen(); @@ -92,6 +100,8 @@ namespace MWMechanics TIterator end() const; + TIterator getActiveSpellById(const ESM::RefId& id); + void update(const MWWorld::Ptr& ptr, float duration); private: @@ -131,7 +141,9 @@ namespace MWMechanics void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects(const MWWorld::Ptr& ptr, const ESM::RefId& id); + void removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); + /// Removes the active effects of a specific active spell + void removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); /// Remove all active effects with this effect id void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {}); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index ec53bdec71..bb3273981d 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -39,12 +39,15 @@ #include "../mwrender/vismask.hpp" +#include "../mwsound/constants.hpp" + #include "actor.hpp" #include "actorutil.hpp" #include "aicombataction.hpp" #include "aifollow.hpp" #include "aipursue.hpp" #include "aiwander.hpp" +#include "attacktype.hpp" #include "character.hpp" #include "creaturestats.hpp" #include "movement.hpp" @@ -211,11 +214,8 @@ namespace const ESM::Static* const fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Soul_Trap")); if (fx != nullptr) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), "", + world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)), "", creature.getRefData().getPosition().asVec3()); - } MWBase::Environment::get().getSoundManager()->playSound3D( creature.getRefData().getPosition().asVec3(), ESM::RefId::stringRefId("conjuration hit"), 1.f, 1.f); @@ -240,6 +240,23 @@ namespace MWMechanics namespace { + std::string_view attackTypeName(AttackType attackType) + { + switch (attackType) + { + case AttackType::NoAttack: + case AttackType::Any: + return {}; + case AttackType::Chop: + return "chop"; + case AttackType::Slash: + return "slash"; + case AttackType::Thrust: + return "thrust"; + } + throw std::logic_error("Invalid attack type value: " + std::to_string(static_cast(attackType))); + } + float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) { @@ -364,7 +381,11 @@ namespace MWMechanics mov.mSpeedFactor = osg::Vec2(controls.mMovement, controls.mSideMovement).length(); stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Run, controls.mRun); stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Sneak, controls.mSneak); - stats.setAttackingOrSpell((controls.mUse & 1) == 1); + + AttackType attackType = static_cast(controls.mUse); + stats.setAttackingOrSpell(attackType != AttackType::NoAttack); + stats.setAttackType(attackTypeName(attackType)); + controls.mChanged = false; } // For the player we don't need to copy these values to Lua because mwinput doesn't change them. @@ -513,7 +534,8 @@ namespace MWMechanics if (greetingTimer >= GREETING_SHOULD_START) { greetingState = Greet_InProgress; - MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello")); + if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello"))) + greetingState = Greet_Done; greetingTimer = 0; } } @@ -580,8 +602,8 @@ namespace MWMechanics } } - void Actors::engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, - std::map>& cachedAllies, bool againstPlayer) const + void Actors::engageCombat( + const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, bool againstPlayer) const { // No combat for totally static creatures if (!actor1.getClass().isMobile(actor1)) @@ -609,9 +631,7 @@ namespace MWMechanics // Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting // those actors, (recursive) and any actor currently being followed or escorted by actor1 - std::set allies1; - - getActorsSidingWith(actor1, allies1, cachedAllies); + const std::set& allies1 = cachedAllies.getActorsSidingWith(actor1); const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); // If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and @@ -623,7 +643,7 @@ namespace MWMechanics if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId())) { - mechanicsManager->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2)); // Also set the same hit attempt actor. Otherwise, if fighting the player, they may stop combat // if the player gets out of reach, while the ally would continue combat with the player creatureStats1.setHitAttemptActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId()); @@ -636,9 +656,8 @@ namespace MWMechanics aggressive = true; } - std::set playerAllies; MWWorld::Ptr player = MWMechanics::getPlayer(); - getActorsSidingWith(player, playerAllies, cachedAllies); + const std::set& playerAllies = cachedAllies.getActorsSidingWith(player); bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end(); @@ -649,20 +668,17 @@ namespace MWMechanics // Check that actor2 is in combat with actor1 if (creatureStats2.getAiSequence().isInCombat(actor1)) { - std::set allies2; - - getActorsSidingWith(actor2, allies2, cachedAllies); - + const std::set& allies2 = cachedAllies.getActorsSidingWith(actor2); // Check that an ally of actor2 is also in combat with actor1 for (const MWWorld::Ptr& ally2 : allies2) { if (ally2 != actor2 && ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) { - mechanicsManager->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2, &allies2); // Also have actor1's allies start combat for (const MWWorld::Ptr& ally1 : allies1) if (ally1 != player) - mechanicsManager->startCombat(ally1, actor2); + mechanicsManager->startCombat(ally1, actor2, &allies2); return; } } @@ -707,10 +723,9 @@ namespace MWMechanics } } - // Make guards go aggressive with creatures that are in combat, unless the creature is a follower or escorter + // Make guards go aggressive with creatures and werewolves that are in combat const auto world = MWBase::Environment::get().getWorld(); - if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && !actor2.getClass().isNpc() - && creatureStats2.getAiSequence().isInCombat()) + if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && creatureStats2.getAiSequence().isInCombat()) { // Check if the creature is too far static const float fAlarmRadius @@ -718,20 +733,30 @@ namespace MWMechanics if (sqrDist > fAlarmRadius * fAlarmRadius) return; - bool followerOrEscorter = false; - for (const auto& package : creatureStats2.getAiSequence()) + bool targetIsCreature = !actor2.getClass().isNpc(); + if (targetIsCreature || actor2.getClass().getNpcStats(actor2).isWerewolf()) { - // The follow package must be first or have nothing but combat before it - if (package->sideWithTarget()) + bool followerOrEscorter = false; + // ...unless the creature has allies + if (targetIsCreature) { - followerOrEscorter = true; - break; + for (const auto& package : creatureStats2.getAiSequence()) + { + // The follow package must be first or have nothing but combat before it + if (package->sideWithTarget()) + { + followerOrEscorter = true; + break; + } + else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) + break; + } } - else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) - break; + // Morrowind also checks "known werewolf" flag, but the player is never in combat + // so this code is unreachable for the player + if (!followerOrEscorter) + aggressive = true; } - if (!followerOrEscorter) - aggressive = true; } // If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, @@ -741,7 +766,7 @@ namespace MWMechanics bool LOS = world->getLOS(actor1, actor2) && mechanicsManager->awarenessCheck(actor2, actor1); if (LOS) - mechanicsManager->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2)); } } @@ -1087,7 +1112,7 @@ namespace MWMechanics } } - void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const + void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const { const MWWorld::Ptr player = getPlayer(); if (ptr == player) @@ -1127,7 +1152,7 @@ namespace MWMechanics = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier) { - mechanicsManager->startCombat(ptr, player); + mechanicsManager->startCombat(ptr, player, &cachedAllies.getActorsSidingWith(player)); creatureStats.setHitAttemptActorId( playerClass.getCreatureStats(player) .getActorId()); // Stops the guard from quitting combat if player is unreachable @@ -1155,6 +1180,9 @@ namespace MWMechanics creatureStats.setAlarmed(false); creatureStats.setAiSetting(AiSetting::Fight, ptr.getClass().getBaseFightRating(ptr)); + // Restore original disposition + npcStats.setCrimeDispositionModifier(0); + // Update witness crime id npcStats.setCrimeId(-1); } @@ -1225,11 +1253,11 @@ namespace MWMechanics } } - void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) const + void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) - iter->second->getCharacterController().castSpell(spellId, manualSpell); + iter->second->getCharacterController().castSpell(spellId, scriptedSpell); } bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const @@ -1285,51 +1313,6 @@ namespace MWMechanics } } - void Actors::updateCombatMusic() - { - const MWWorld::Ptr player = getPlayer(); - const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); - bool hasHostiles = false; // need to know this to play Battle music - const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); - - if (aiActive) - { - const int actorsProcessingRange = Settings::game().mActorsProcessingRange; - for (const Actor& actor : mActors) - { - if (actor.getPtr() == player) - continue; - - const bool inProcessingRange - = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2() - <= actorsProcessingRange * actorsProcessingRange; - if (inProcessingRange) - { - MWMechanics::CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); - if (!stats.isDead() && stats.getAiSequence().isInCombat()) - { - hasHostiles = true; - break; - } - } - } - } - - // check if we still have any player enemies to switch music - if (mCurrentMusic != MusicType::Explore && !hasHostiles - && !(player.getClass().getCreatureStats(player).isDead() - && MWBase::Environment::get().getSoundManager()->isMusicPlaying())) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); - mCurrentMusic = MusicType::Explore; - } - else if (mCurrentMusic != MusicType::Battle && hasHostiles) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); - mCurrentMusic = MusicType::Battle; - } - } - void Actors::predictAndAvoidCollisions(float duration) const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) @@ -1511,8 +1494,7 @@ namespace MWMechanics /// \todo move update logic to Actor class where appropriate - std::map> - cachedAllies; // will be filled as engageCombat iterates + SidingCache cachedAllies{ *this, true }; // will be filled as engageCombat iterates const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); const int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); @@ -1523,7 +1505,6 @@ namespace MWMechanics if (!playerHitAttemptActor.isInCell()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - const bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); const int actorsProcessingRange = Settings::game().mActorsProcessingRange; // AI and magic effects update @@ -1600,7 +1581,7 @@ namespace MWMechanics updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl); if (actor.getPtr().getClass().isNpc() && !isPlayer) - updateCrimePursuit(actor.getPtr(), duration); + updateCrimePursuit(actor.getPtr(), duration, cachedAllies); if (!isPlayer) { @@ -1675,8 +1656,7 @@ namespace MWMechanics world->setActorActive(actor.getPtr(), true); const bool isDead = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead(); - if (!isDead && (!godmode || !isPlayer) - && actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isParalyzed()) + if (!isDead && actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isParalyzed()) ctrl.skipAnim(); // Handle player last, in case a cell transition occurs by casting a teleportation spell @@ -1735,8 +1715,6 @@ namespace MWMechanics killDeadActors(); updateSneaking(playerCharacter, duration); } - - updateCombatMusic(); } void Actors::notifyDied(const MWWorld::Ptr& actor) @@ -1744,6 +1722,8 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).notifyDied(); ++mDeathCount[actor.getCellRef().getRefId()]; + + MWBase::Environment::get().getLuaManager()->actorDied(actor); } void Actors::resurrect(const MWWorld::Ptr& ptr) const @@ -1805,8 +1785,6 @@ namespace MWMechanics { // player's death animation is over MWBase::Environment::get().getStateManager()->askLoadRecent(); - // Play Death Music if it was the player dying - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } else { @@ -1827,12 +1805,9 @@ namespace MWMechanics const ESM::Static* fx = MWBase::Environment::get().getESMStore()->get().search( ESM::RefId::stringRefId("VFX_Summon_End")); if (fx) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); MWBase::Environment::get().getWorld()->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), "", + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)), "", ptr.getRefData().getPosition().asVec3()); - } // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); @@ -1889,6 +1864,9 @@ namespace MWMechanics > actorsProcessingRange * actorsProcessingRange) continue; + // Get rid of effects pending removal so they are not applied when resting + updateMagicEffects(actor.getPtr()); + adjustMagicEffects(actor.getPtr(), duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(actor.getPtr()); @@ -1968,7 +1946,7 @@ namespace MWMechanics mSneakSkillTimer = 0.f; if (avoidedNotice && mSneakSkillTimer == 0.f) - player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 0); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_AvoidNotice); if (!detected) MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); @@ -2011,12 +1989,12 @@ namespace MWMechanics } bool Actors::playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist) const + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { - return iter->second->getCharacterController().playGroup(groupName, mode, number, persist); + return iter->second->getCharacterController().playGroup(groupName, mode, number, scripted); } else { @@ -2025,6 +2003,24 @@ namespace MWMechanics return false; } } + + bool Actors::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().playGroupLua( + groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Actors::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().enableLuaAnimations(enable); + } + void Actors::skipAnimation(const MWWorld::Ptr& ptr) const { const auto iter = mIndex.find(ptr.mRef); @@ -2040,12 +2036,27 @@ namespace MWMechanics return false; } + bool Actors::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().isScriptedAnimPlaying(); + return false; + } + void Actors::persistAnimationStates() const { for (const Actor& actor : mActors) actor.getCharacterController().persistAnimationState(); } + void Actors::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().clearAnimQueue(clearScripted); + } + void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { for (const Actor& actor : mActors) @@ -2088,7 +2099,7 @@ namespace MWMechanics for (const auto& package : stats.getAiSequence()) { if (excludeInfighting && !sameActor && package->getTypeId() == AiPackageTypeId::Combat - && package->getTarget() == actorPtr) + && package->targetIs(actorPtr)) break; if (package->sideWithTarget() && !package->getTarget().isEmpty()) { @@ -2101,10 +2112,14 @@ namespace MWMechanics if (ally.getClass().getCreatureStats(ally).getAiSequence().getCombatTargets(enemies) && std::find(enemies.begin(), enemies.end(), actorPtr) != enemies.end()) break; + enemies.clear(); + if (actorPtr.getClass().getCreatureStats(actorPtr).getAiSequence().getCombatTargets(enemies) + && std::find(enemies.begin(), enemies.end(), ally) != enemies.end()) + break; } list.push_back(package->getTarget()); } - else if (package->getTarget() == actorPtr) + else if (package->targetIs(actorPtr)) { list.push_back(iteratedActor); } @@ -2123,7 +2138,7 @@ namespace MWMechanics std::vector list; forEachFollowingPackage( mActors, actorPtr, getPlayer(), [&](const Actor& actor, const std::shared_ptr& package) { - if (package->followTargetThroughDoors() && package->getTarget() == actorPtr) + if (package->followTargetThroughDoors() && package->targetIs(actorPtr)) list.push_back(actor.getPtr()); else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) @@ -2150,38 +2165,12 @@ namespace MWMechanics getActorsSidingWith(follower, out, excludeInfighting); } - void Actors::getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out, - std::map>& cachedAllies) const - { - // If we have already found actor's allies, use the cache - std::map>::const_iterator search = cachedAllies.find(actor); - if (search != cachedAllies.end()) - out.insert(search->second.begin(), search->second.end()); - else - { - for (const MWWorld::Ptr& follower : getActorsSidingWith(actor, true)) - if (out.insert(follower).second && follower != actor) - getActorsSidingWith(follower, out, cachedAllies); - - // Cache ptrs and their sets of allies - cachedAllies.insert(std::make_pair(actor, out)); - for (const MWWorld::Ptr& iter : out) - { - if (iter == actor) - continue; - search = cachedAllies.find(iter); - if (search == cachedAllies.end()) - cachedAllies.insert(std::make_pair(iter, out)); - } - } - } - std::vector Actors::getActorsFollowingIndices(const MWWorld::Ptr& actor) const { std::vector list; forEachFollowingPackage( mActors, actor, getPlayer(), [&](const Actor&, const std::shared_ptr& package) { - if (package->followTargetThroughDoors() && package->getTarget() == actor) + if (package->followTargetThroughDoors() && package->targetIs(actor)) { list.push_back(static_cast(package.get())->getFollowIndex()); return false; @@ -2199,7 +2188,7 @@ namespace MWMechanics std::map map; forEachFollowingPackage( mActors, actor, getPlayer(), [&](const Actor& otherActor, const std::shared_ptr& package) { - if (package->followTargetThroughDoors() && package->getTarget() == actor) + if (package->followTargetThroughDoors() && package->targetIs(actor)) { const int index = static_cast(package.get())->getFollowIndex(); map[index] = otherActor.getPtr(); @@ -2374,4 +2363,32 @@ namespace MWMechanics seq.fastForward(ptr); } } + + const std::set& SidingCache::getActorsSidingWith(const MWWorld::Ptr& actor) + { + // If we have already found actor's allies, use the cache + auto search = mCache.find(actor); + if (search != mCache.end()) + return search->second; + std::set& out = mCache[actor]; + for (const MWWorld::Ptr& follower : mActors.getActorsSidingWith(actor, mExcludeInfighting)) + { + if (out.insert(follower).second && follower != actor) + { + const auto& allies = getActorsSidingWith(follower); + out.insert(allies.begin(), allies.end()); + } + } + + // Cache ptrs and their sets of allies + for (const MWWorld::Ptr& iter : out) + { + if (iter == actor) + continue; + search = mCache.find(iter); + if (search == mCache.end()) + mCache.emplace(iter, out); + } + return out; + } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 1c5799159e..b575ec2827 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -36,6 +36,7 @@ namespace MWMechanics class Actor; class CharacterController; class CreatureStats; + class SidingCache; class Actors { @@ -66,7 +67,7 @@ namespace MWMechanics void resurrect(const MWWorld::Ptr& ptr) const; - void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell = false) const; + void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) const; void updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const; ///< Updates an actor with a new Ptr @@ -74,9 +75,6 @@ namespace MWMechanics void dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore); ///< Deregister all actors (except for \a ignore) in the given cell. - void updateCombatMusic(); - ///< Update combat music state - void update(float duration, bool paused); ///< Update actor stats and store desired velocity vectors in \a movement @@ -115,11 +113,16 @@ namespace MWMechanics void forceStateUpdate(const MWWorld::Ptr& ptr) const; - bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist = false) const; + bool playAnimationGroup(const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, + bool scripted = false) const; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr) const; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; + bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const; void persistAnimationStates() const; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; @@ -165,13 +168,6 @@ namespace MWMechanics bool isTurningToPlayer(const MWWorld::Ptr& ptr) const; private: - enum class MusicType - { - Title, - Explore, - Battle - }; - std::map mDeathCount; std::list mActors; std::map::iterator> mIndex; @@ -182,7 +178,6 @@ namespace MWMechanics float mTimerUpdateHello = 0; float mSneakTimer = 0; // Times update of sneak icon float mSneakSkillTimer = 0; // Times sneak skill progress from "avoid notice" - MusicType mCurrentMusic = MusicType::Title; void updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const; @@ -190,7 +185,7 @@ namespace MWMechanics void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const; - void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const; + void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const; void killDeadActors(); @@ -202,13 +197,25 @@ namespace MWMechanics @Notes: If againstPlayer = true then actor2 should be the Player. If one of the combatants is creature it should be actor1. */ - void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, - std::map>& cachedAllies, bool againstPlayer) const; + void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, + bool againstPlayer) const; + }; - /// Recursive version of getActorsSidingWith that takes, adds to and returns a cache of - /// actors mapped to their allies. Excludes infighting - void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out, - std::map>& cachedAllies) const; + class SidingCache + { + const Actors& mActors; + const bool mExcludeInfighting; + std::map> mCache; + + public: + SidingCache(const Actors& actors, bool excludeInfighting) + : mActors(actors) + , mExcludeInfighting(excludeInfighting) + { + } + + /// Recursive version of getActorsSidingWith that takes, returns a cached set of allies + const std::set& getActorsSidingWith(const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index c414ff3032..2d2980075e 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -39,6 +39,6 @@ namespace MWMechanics { const MagicEffects& magicEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); return (magicEffects.getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0) - || (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() > 75); + || (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() >= 75); } } diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 31abda44c2..be4fe5e674 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -30,7 +30,7 @@ namespace MWMechanics // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) return true; // Turn to target and move to it directly, without pathfinding. diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp index 249ca97326..6384d70c06 100644 --- a/apps/openmw/mwmechanics/aicast.cpp +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -25,11 +25,11 @@ namespace MWMechanics } } -MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool manualSpell) +MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell) : mTargetId(targetId) , mSpellId(spellId) , mCasting(false) - , mManual(manualSpell) + , mScripted(scriptedSpell) , mDistance(getInitialDistance(spellId)) { } @@ -49,7 +49,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac if (target.isEmpty()) return true; - if (!mManual + if (!mScripted && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, characterController.getSupportedMovementDirections(), mDistance)) { @@ -85,7 +85,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac if (!mCasting) { - MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); + MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mScripted); mCasting = true; return false; } diff --git a/apps/openmw/mwmechanics/aicast.hpp b/apps/openmw/mwmechanics/aicast.hpp index 435458cc0f..649c5a4d34 100644 --- a/apps/openmw/mwmechanics/aicast.hpp +++ b/apps/openmw/mwmechanics/aicast.hpp @@ -15,7 +15,7 @@ namespace MWMechanics class AiCast final : public TypedAiPackage { public: - AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool manualSpell = false); + AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell = false); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; @@ -37,7 +37,7 @@ namespace MWMechanics const ESM::RefId mTargetId; const ESM::RefId mSpellId; bool mCasting; - const bool mManual; + const bool mScripted; const float mDistance; }; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index dbe83eab42..2399961a3a 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -23,6 +23,7 @@ #include "actorutil.hpp" #include "aicombataction.hpp" #include "character.hpp" +#include "combat.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "pathgrid.hpp" @@ -114,7 +115,7 @@ namespace MWMechanics if (target.isEmpty()) return true; - if (!target.getRefData().getCount() + if (!target.getCellRef().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently // registered with the MechanicsManager || target.getClass().getCreatureStats(target).isDead()) @@ -242,7 +243,7 @@ namespace MWMechanics const osg::Vec3f vActorPos(pos.asVec3()); const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); - float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); + float distToTarget = getDistanceToBounds(actor, target); storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS); @@ -269,7 +270,15 @@ namespace MWMechanics { storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); // start new attack - storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); + bool canShout = true; + ESM::RefId spellId = storage.mCurrentAction->getSpell(); + if (!spellId.empty()) + { + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); + if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mData.mRange != ESM::RT_Target) + canShout = false; + } + storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout); } // If actor uses custom destination it has to try to rebuild path because environment can change @@ -282,8 +291,8 @@ namespace MWMechanics const MWBase::World* world = MWBase::Environment::get().getWorld(); // Try to build path to the target. const auto agentBounds = world->getPathfindingAgentBounds(actor); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const auto& pathGridGraph = getPathGridGraph(pathgrid); mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, @@ -477,8 +486,7 @@ namespace MWMechanics MWWorld::Ptr AiCombat::getTarget() const { - if (mCachedTarget.isEmpty() || mCachedTarget.getRefData().isDeleted() - || !mCachedTarget.getRefData().isEnabled()) + if (mCachedTarget.isEmpty() || mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) { mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); } @@ -635,7 +643,7 @@ namespace MWMechanics } void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, - const ESM::Weapon* weapon, bool distantCombat) + const ESM::Weapon* weapon, bool distantCombat, bool canShout) { if (mReadyToAttack && characterController.readyToStartAttack()) { @@ -658,12 +666,15 @@ namespace MWMechanics baseDelay = store.get().find("fCombatDelayNPC")->mValue.getFloat(); } - // Say a provoking combat phrase - const int iVoiceAttackOdds - = store.get().find("iVoiceAttackOdds")->mValue.getInteger(); - if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds) + if (canShout) { - MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack")); + // Say a provoking combat phrase + const int iVoiceAttackOdds + = store.get().find("iVoiceAttackOdds")->mValue.getInteger(); + if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds) + { + MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack")); + } } mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(prng), baseDelay + 0.9); } @@ -707,7 +718,7 @@ namespace MWMechanics mFleeDest = ESM::Pathgrid::Point(0, 0, 0); } - bool AiCombatStorage::isFleeing() + bool AiCombatStorage::isFleeing() const { return mFleeState != FleeState_None; } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 92d380dbd8..d5a9c3464c 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -64,13 +64,13 @@ namespace MWMechanics void updateCombatMove(float duration); void stopCombatMove(); void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, - const ESM::Weapon* weapon, bool distantCombat); + const ESM::Weapon* weapon, bool distantCombat, bool canShout); void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController); void stopAttack(); void startFleeing(); void stopFleeing(); - bool isFleeing(); + bool isFleeing() const; }; /// \brief Causes the actor to fight another actor diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index a7bcd1a0df..91d2a9bbb8 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -176,34 +176,35 @@ namespace MWMechanics return bestAction; } - if (actor.getClass().hasInventoryStore(actor)) + const bool hasInventoryStore = actor.getClass().hasInventoryStore(actor); + MWWorld::ContainerStore& store = actor.getClass().getContainerStore(actor); + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + if (it->getType() == ESM::Potion::sRecordId) { - if (it->getType() == ESM::Potion::sRecordId) + float rating = ratePotion(*it, actor); + if (rating > bestActionRating) { - float rating = ratePotion(*it, actor); - if (rating > bestActionRating) - { - bestActionRating = rating; - bestAction = std::make_unique(*it); - antiFleeRating = std::numeric_limits::max(); - } - } - else if (!it->getClass().getEnchantment(*it).empty()) - { - float rating = rateMagicItem(*it, actor, enemy); - if (rating > bestActionRating) - { - bestActionRating = rating; - bestAction = std::make_unique(it); - antiFleeRating = std::numeric_limits::max(); - } + bestActionRating = rating; + bestAction = std::make_unique(*it); + antiFleeRating = std::numeric_limits::max(); } } + // TODO remove inventory store check, creatures should be able to use enchanted items they cannot equip + else if (hasInventoryStore && !it->getClass().getEnchantment(*it).empty()) + { + float rating = rateMagicItem(*it, actor, enemy); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction = std::make_unique(it); + antiFleeRating = std::numeric_limits::max(); + } + } + } + if (hasInventoryStore) + { MWWorld::Ptr bestArrow; float bestArrowRating = rateAmmo(actor, enemy, bestArrow, ESM::Weapon::Arrow); @@ -354,14 +355,14 @@ namespace MWMechanics { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(selectedSpellId); - for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - effectIt->mEffectID); + effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } @@ -374,14 +375,14 @@ namespace MWMechanics { const ESM::Enchantment* ench = MWBase::Environment::get().getESMStore()->get().find(enchId); - for (std::vector::const_iterator effectIt = ench->mEffects.mList.begin(); + for (std::vector::const_iterator effectIt = ench->mEffects.mList.begin(); effectIt != ench->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - effectIt->mEffectID); + effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index 05d2a18e25..9eb7df211a 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -16,6 +16,7 @@ namespace MWMechanics virtual float getCombatRange(bool& isRanged) const = 0; virtual float getActionCooldown() const { return 0.f; } virtual const ESM::Weapon* getWeapon() const { return nullptr; } + virtual ESM::RefId getSpell() const { return {}; } virtual bool isAttackingOrSpell() const { return true; } virtual bool isFleeing() const { return false; } }; @@ -43,6 +44,7 @@ namespace MWMechanics void prepare(const MWWorld::Ptr& actor) override; float getCombatRange(bool& isRanged) const override; + ESM::RefId getSpell() const override { return mSpellId; } }; class ActionEnchantedItem : public Action diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index e1d657a207..9e6df46340 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -1,5 +1,6 @@ #include "aiescort.hpp" +#include #include #include #include @@ -30,8 +31,6 @@ namespace MWMechanics , mZ(z) , mDuration(duration) , mRemainingDuration(static_cast(duration)) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) { mTargetActorRefId = actorId; } @@ -45,8 +44,6 @@ namespace MWMechanics , mZ(z) , mDuration(duration) , mRemainingDuration(static_cast(duration)) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) { mTargetActorRefId = actorId; } @@ -59,8 +56,6 @@ namespace MWMechanics , mZ(escort->mData.mZ) , mDuration(escort->mData.mDuration) , mRemainingDuration(escort->mRemainingDuration) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) { mTargetActorRefId = escort->mTargetId; mTargetActorId = escort->mTargetActorId; @@ -96,6 +91,19 @@ namespace MWMechanics if ((leaderPos - followerPos).length2() <= mMaxDist * mMaxDist) { + // TESCS allows the creation of Escort packages without a specific destination + constexpr float nowhere = std::numeric_limits::max(); + if (mX == nowhere || mY == nowhere) + return true; + if (mZ == nowhere) + { + if (mCellId.empty() + && ESM::positionToExteriorCellLocation(mX, mY) + == actor.getCell()->getCell()->getExteriorCellLocation()) + return false; + return true; + } + const osg::Vec3f dest(mX, mY, mZ); if (pathTo(actor, dest, duration, characterController.getSupportedMovementDirections(), maxHalfExtent)) { diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index e22752446d..d88ecac6a5 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -51,6 +51,8 @@ namespace MWMechanics osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } + std::optional getDuration() const override { return mDuration; } + private: const std::string mCellId; const float mX; @@ -59,9 +61,6 @@ namespace MWMechanics float mMaxDist = 450; const float mDuration; // In hours float mRemainingDuration; // In hours - - const int mCellX; - const int mCellY; }; } #endif diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index b78c1fd6ee..b4779dc900 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -99,7 +99,7 @@ namespace MWMechanics // Target is not here right now, wait for it to return // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) return false; actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); @@ -119,21 +119,6 @@ namespace MWMechanics const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); const osg::Vec3f targetDir = targetPos - actorPos; - // AiFollow requires the target to be in range and within sight for the initial activation - if (!mActive) - { - storage.mTimer -= duration; - - if (storage.mTimer < 0) - { - if (targetDir.length2() < 500 * 500 && MWBase::Environment::get().getWorld()->getLOS(actor, target)) - mActive = true; - storage.mTimer = 0.5f; - } - } - if (!mActive) - return false; - // In the original engine the first follower stays closer to the player than any subsequent followers. // Followers beyond the first usually attempt to stand inside each other. osg::Vec3f::value_type floatingDistance = 0; @@ -152,6 +137,23 @@ namespace MWMechanics floatingDistance += getHalfExtents(actor) * 2; short followDistance = static_cast(floatingDistance); + // AiFollow requires the target to be in range and within sight for the initial activation + if (!mActive) + { + storage.mTimer -= duration; + + if (storage.mTimer < 0) + { + float activeRange = followDistance + 384.f; + if (targetDir.length2() < activeRange * activeRange + && MWBase::Environment::get().getWorld()->getLOS(actor, target)) + mActive = true; + storage.mTimer = 0.5f; + } + } + if (!mActive) + return false; + if (!mAlwaysFollow) // Update if you only follow for a bit { // Check if we've run out of time diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 183c30bfb7..4bcfc7dedd 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -9,6 +9,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" @@ -63,7 +64,7 @@ MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { if (!mCachedTarget.isEmpty()) { - if (mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + if (mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) mCachedTarget = MWWorld::Ptr(); else return mCachedTarget; @@ -97,6 +98,26 @@ MWWorld::Ptr MWMechanics::AiPackage::getTarget() const return mCachedTarget; } +bool MWMechanics::AiPackage::targetIs(const MWWorld::Ptr& ptr) const +{ + if (mTargetActorId == -2) + return ptr.isEmpty(); + else if (mTargetActorId == -1) + { + if (mTargetActorRefId.empty()) + { + mTargetActorId = -2; + return ptr.isEmpty(); + } + if (!ptr.isEmpty() && ptr.getCellRef().getRefId() == mTargetActorRefId) + return getTarget() == ptr; + return false; + } + if (ptr.isEmpty() || !ptr.getClass().isActor()) + return false; + return ptr.getClass().getCreatureStats(ptr).getActorId() == mTargetActorId; +} + void MWMechanics::AiPackage::reset() { // reset all members @@ -120,12 +141,12 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& MWBase::World* world = MWBase::Environment::get().getWorld(); const DetourNavigator::AgentBounds agentBounds = world->getPathfindingAgentBounds(actor); - /// Stops the actor when it gets too close to a unloaded cell - //... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" - // setting value - //... units from player, and exterior cells are 8192 units long and wide. + /// Stops the actor when it gets too close to a unloaded cell or when the actor is playing a scripted animation + //... At current time, the first test is unnecessary. AI shuts down when actor is more than + //... "actors processing range" setting value units from player, and exterior cells are 8192 units long and wide. //... But AI processing distance may increase in the future. - if (isNearInactiveCell(position)) + if (isNearInactiveCell(position) + || MWBase::Environment::get().getMechanicsManager()->checkScriptedAnimationPlaying(actor)) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; @@ -157,8 +178,10 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& { const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, getNavigatorFlags(actor), getAreaCosts(actor), endTolerance, pathType); + agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -486,14 +509,12 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: return result; } -DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::Ptr& actor) const +DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts( + const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const { DetourNavigator::AreaCosts costs; - const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); - const float swimSpeed = (flags & DetourNavigator::Flag_swim) == 0 ? 0.0f : actorClass.getSwimSpeed(actor); - const float walkSpeed = [&] { if ((flags & DetourNavigator::Flag_walk) == 0) return 0.0f; @@ -502,6 +523,14 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P return actorClass.getRunSpeed(actor); }(); + const float swimSpeed = [&] { + if ((flags & DetourNavigator::Flag_swim) == 0) + return 0.0f; + if (hasWaterWalking(actor)) + return walkSpeed; + return actorClass.getSwimSpeed(actor); + }(); + const float maxSpeed = std::max(swimSpeed, walkSpeed); if (maxSpeed == 0) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index fa018609e4..ca33f5dc90 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -87,6 +87,8 @@ namespace MWMechanics /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) virtual MWWorld::Ptr getTarget() const; + /// Optimized version of getTarget() == ptr + virtual bool targetIs(const MWWorld::Ptr& ptr) const; /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); } @@ -108,6 +110,10 @@ namespace MWMechanics virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } + virtual std::optional getDistance() const { return std::nullopt; } + + virtual std::optional getDuration() const { return std::nullopt; } + /// Return true if any loaded actor with this AI package must be active. bool alwaysActive() const { return mOptions.mAlwaysActive; } @@ -150,7 +156,7 @@ namespace MWMechanics DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const; - DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor) const; + DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const; const AiPackageTypeId mTypeId; const Options mOptions; diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 2ae4ce5def..461db45133 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -12,6 +12,7 @@ #include "actorutil.hpp" #include "character.hpp" #include "creaturestats.hpp" +#include "npcstats.hpp" namespace MWMechanics { @@ -37,7 +38,7 @@ namespace MWMechanics // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) return true; if (isTargetMagicallyHidden(target) @@ -47,6 +48,9 @@ namespace MWMechanics if (target.getClass().getCreatureStats(target).isDead()) return true; + if (target.getClass().getNpcStats(target).getBounty() <= 0) + return true; + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); // Set the target destination @@ -79,7 +83,7 @@ namespace MWMechanics { if (!mCachedTarget.isEmpty()) { - if (mCachedTarget.getRefData().isDeleted() || !mCachedTarget.getRefData().isEnabled()) + if (mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) mCachedTarget = MWWorld::Ptr(); else return mCachedTarget; diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index b58927c993..019aaf7c0a 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -6,6 +6,8 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "actorutil.hpp" #include "aiactivate.hpp" @@ -17,6 +19,7 @@ #include "aipursue.hpp" #include "aitravel.hpp" #include "aiwander.hpp" +#include "creaturestats.hpp" namespace MWMechanics { @@ -79,7 +82,11 @@ namespace MWMechanics void AiSequence::onPackageRemoved(const AiPackage& package) { if (package.getTypeId() == AiPackageTypeId::Combat) + { mNumCombatPackages--; + if (mNumCombatPackages == 0) + mResetFriendlyHits = true; + } else if (package.getTypeId() == AiPackageTypeId::Pursue) mNumPursuitPackages--; @@ -135,6 +142,15 @@ namespace MWMechanics return mNumPursuitPackages > 0; } + bool AiSequence::isFleeing() const + { + if (!isInCombat()) + return false; + + const AiCombatStorage* storage = mAiState.getPtr(); + return storage && storage->isFleeing(); + } + bool AiSequence::isEngagedWithActor() const { if (!isInCombat()) @@ -164,11 +180,11 @@ namespace MWMechanics if (!isInCombat()) return false; - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + for (const auto& package : mPackages) { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) + if (package->getTypeId() == AiPackageTypeId::Combat) { - if ((*it)->getTarget() == actor) + if (package->targetIs(actor)) return true; } } @@ -234,6 +250,12 @@ namespace MWMechanics return; } + if (mResetFriendlyHits) + { + actor.getClass().getCreatureStats(actor).resetFriendlyHits(); + mResetFriendlyHits = false; + } + if (mPackages.empty()) { mLastAiPackage = AiPackageTypeId::None; @@ -272,7 +294,9 @@ namespace MWMechanics } else { - float rating = MWMechanics::getBestActionRating(actor, target); + float rating = 0.f; + if (MWMechanics::canFight(actor, target)) + rating = MWMechanics::getBestActionRating(actor, target); const ESM::Position& targetPos = target.getRefData().getPosition(); @@ -354,7 +378,20 @@ namespace MWMechanics // Stop combat when a non-combat AI package is added if (isActualAiPackage(package.getTypeId())) + { + if (package.getTypeId() == MWMechanics::AiPackageTypeId::Follow + || package.getTypeId() == MWMechanics::AiPackageTypeId::Escort) + { + const auto& mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + std::vector newAllies = mechanicsManager->getActorsSidingWith(package.getTarget()); + std::vector allies = mechanicsManager->getActorsSidingWith(actor); + for (const auto& ally : allies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(newAllies); + for (const auto& ally : newAllies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(allies); + } stopCombat(); + } // We should return a wandering actor back after combat, casting or pursuit. // The same thing for actors without AI packages. @@ -456,7 +493,7 @@ namespace MWMechanics { ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(ESM::RefId::stringRefId(data.mId.toStringView()), - data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + esmPackage.mCellName, data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Travel) { @@ -473,7 +510,7 @@ namespace MWMechanics { ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(ESM::RefId::stringRefId(data.mId.toStringView()), - data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + esmPackage.mCellName, data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } onPackageAdded(*package); diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index ab3cc11e2c..5e7e521a40 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -39,6 +39,7 @@ namespace MWMechanics /// Finished with top AIPackage, set for one frame bool mDone{}; + bool mResetFriendlyHits{}; int mNumCombatPackages{}; int mNumPursuitPackages{}; @@ -117,6 +118,9 @@ namespace MWMechanics /// Is there any pursuit package. bool isInPursuit() const; + /// Is the actor fleeing? + bool isFleeing() const; + /// Removes all packages using the specified id. void removePackagesById(AiPackageTypeId id); @@ -129,9 +133,6 @@ namespace MWMechanics /// Are we in combat with this particular actor? bool isInCombat(const MWWorld::Ptr& actor) const; - bool canAddTarget(const ESM::Position& actorPos, float distToTarget) const; - ///< Function assumes that actor can have only 1 target apart player - /// Removes all combat packages until first non-combat or stack empty. void stopCombat(); diff --git a/apps/openmw/mwmechanics/aistate.hpp b/apps/openmw/mwmechanics/aistate.hpp index d79469a9a0..f2ce17fd9c 100644 --- a/apps/openmw/mwmechanics/aistate.hpp +++ b/apps/openmw/mwmechanics/aistate.hpp @@ -38,6 +38,13 @@ namespace MWMechanics return *result; } + /// \brief returns pointer to stored object in the desired type + template + Derived* getPtr() const + { + return dynamic_cast(mStorage.get()); + } + template void copy(DerivedClassStorage& destination) const { diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 30756ade35..3c299c1490 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -86,7 +86,7 @@ namespace MWMechanics return MWBase::Environment::get() .getWorld() ->getRayCasting() - ->castRay(position, visibleDestination, actor, {}, mask) + ->castRay(position, visibleDestination, { actor }, {}, mask) .mHit; } @@ -224,12 +224,14 @@ namespace MWMechanics { const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, getNavigatorFlags(actor), getAreaCosts(actor), endTolerance, PathType::Full); + agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) - storage.setState(AiWanderStorage::Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); } if (!cStats.getMovementFlag(CreatureStats::Flag_ForceJump) @@ -367,8 +369,8 @@ namespace MWMechanics const auto world = MWBase::Environment::get().getWorld(); const auto agentBounds = world->getPathfindingAgentBounds(actor); const auto navigator = world->getNavigator(); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); do @@ -453,27 +455,37 @@ namespace MWMechanics void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage) { - switch (storage.mState) + // Attempt to fast forward to the next state instead of remaining in an intermediate state for a frame + for (int i = 0; i < 2; ++i) { - case AiWanderStorage::Wander_IdleNow: - onIdleStatePerFrameActions(actor, duration, storage); - break; + switch (storage.mState) + { + case AiWanderStorage::Wander_IdleNow: + { + onIdleStatePerFrameActions(actor, duration, storage); + if (storage.mState != AiWanderStorage::Wander_ChooseAction) + return; + continue; + } + case AiWanderStorage::Wander_Walking: + onWalkingStatePerFrameActions(actor, duration, supportedMovementDirections, storage); + return; - case AiWanderStorage::Wander_Walking: - onWalkingStatePerFrameActions(actor, duration, supportedMovementDirections, storage); - break; + case AiWanderStorage::Wander_ChooseAction: + { + onChooseActionStatePerFrameActions(actor, storage); + if (storage.mState != AiWanderStorage::Wander_IdleNow) + return; + continue; + } + case AiWanderStorage::Wander_MoveNow: + return; // nothing to do - case AiWanderStorage::Wander_ChooseAction: - onChooseActionStatePerFrameActions(actor, storage); - break; - - case AiWanderStorage::Wander_MoveNow: - break; // nothing to do - - default: - // should never get here - assert(false); - break; + default: + // should never get here + assert(false); + return; + } } } @@ -499,7 +511,7 @@ namespace MWMechanics if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) { if (mPathFinder.isPathConstructed()) - storage.setState(AiWanderStorage::Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); else storage.setState(AiWanderStorage::Wander_ChooseAction); } diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 6d5bd7f8cd..aed7214f4d 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -113,6 +113,14 @@ namespace MWMechanics bool isStationary() const { return mDistance == 0; } + std::optional getDistance() const override { return mDistance; } + + std::optional getDuration() const override { return static_cast(mDuration); } + + const std::vector& getIdle() const { return mIdle; } + + static std::string_view getIdleGroupName(size_t index) { return sIdleSelectToGroupName[index]; } + private: void stopWalking(const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 914de6da2b..4be48296a9 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -24,45 +24,64 @@ #include "creaturestats.hpp" #include "magiceffects.hpp" +namespace +{ + constexpr size_t sNumEffects = 4; + + std::optional toKey(const ESM::Ingredient& ingredient, size_t i) + { + if (ingredient.mData.mEffectID[i] < 0) + return {}; + ESM::RefId arg = ESM::Skill::indexToRefId(ingredient.mData.mSkills[i]); + if (arg.empty()) + arg = ESM::Attribute::indexToRefId(ingredient.mData.mAttributes[i]); + return MWMechanics::EffectKey(ingredient.mData.mEffectID[i], arg); + } + + bool containsEffect(const ESM::Ingredient& ingredient, const MWMechanics::EffectKey& effect) + { + for (size_t j = 0; j < sNumEffects; ++j) + { + if (toKey(ingredient, j) == effect) + return true; + } + return false; + } +} + MWMechanics::Alchemy::Alchemy() : mValue(0) - , mPotionName("") { } -std::set MWMechanics::Alchemy::listEffects() const +std::vector MWMechanics::Alchemy::listEffects() const { - std::map effects; - - for (TIngredientsIterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter) + // We care about the order of these effects as each effect can affect the next when applied. + // The player can affect effect order by placing ingredients into different slots + std::vector effects; + for (size_t slotI = 0; slotI < mIngredients.size() - 1; ++slotI) { - if (!iter->isEmpty()) + if (mIngredients[slotI].isEmpty()) + continue; + const ESM::Ingredient* ingredient = mIngredients[slotI].get()->mBase; + for (size_t slotJ = slotI + 1; slotJ < mIngredients.size(); ++slotJ) { - const MWWorld::LiveCellRef* ingredient = iter->get(); - - std::set seenEffects; - - for (int i = 0; i < 4; ++i) - if (ingredient->mBase->mData.mEffectID[i] != -1) + if (mIngredients[slotJ].isEmpty()) + continue; + const ESM::Ingredient* ingredient2 = mIngredients[slotJ].get()->mBase; + for (size_t i = 0; i < sNumEffects; ++i) + { + if (const auto key = toKey(*ingredient, i)) { - ESM::RefId arg = ESM::Skill::indexToRefId(ingredient->mBase->mData.mSkills[i]); - if (arg.empty()) - arg = ESM::Attribute::indexToRefId(ingredient->mBase->mData.mAttributes[i]); - EffectKey key(ingredient->mBase->mData.mEffectID[i], arg); - - if (seenEffects.insert(key).second) - ++effects[key]; + if (std::find(effects.begin(), effects.end(), *key) != effects.end()) + continue; + if (containsEffect(*ingredient2, *key)) + effects.push_back(*key); } + } } } - - std::set effects2; - - for (std::map::const_iterator iter(effects.begin()); iter != effects.end(); ++iter) - if (iter->second > 1) - effects2.insert(iter->first); - - return effects2; + return effects; } void MWMechanics::Alchemy::applyTools(int flags, float& value) const @@ -133,7 +152,7 @@ void MWMechanics::Alchemy::updateEffects() return; // find effects - std::set effects(listEffects()); + std::vector effects = listEffects(); // general alchemy factor float x = getAlchemyFactor(); @@ -150,14 +169,14 @@ void MWMechanics::Alchemy::updateEffects() x * MWBase::Environment::get().getESMStore()->get().find("iAlchemyMod")->mValue.getFloat()); // build quantified effect list - for (std::set::const_iterator iter(effects.begin()); iter != effects.end(); ++iter) + for (const auto& effectKey : effects) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(iter->mId); + = MWBase::Environment::get().getESMStore()->get().find(effectKey.mId); if (magicEffect->mData.mBaseCost <= 0) { - const std::string os = "invalid base cost for magic effect " + std::to_string(iter->mId); + const std::string os = "invalid base cost for magic effect " + std::to_string(effectKey.mId); throw std::runtime_error(os); } @@ -198,15 +217,15 @@ void MWMechanics::Alchemy::updateEffects() if (magnitude > 0 && duration > 0) { ESM::ENAMstruct effect; - effect.mEffectID = iter->mId; + effect.mEffectID = effectKey.mId; effect.mAttribute = -1; effect.mSkill = -1; if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) - effect.mSkill = ESM::Skill::refIdToIndex(iter->mArg); + effect.mSkill = ESM::Skill::refIdToIndex(effectKey.mArg); else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) - effect.mAttribute = ESM::Attribute::refIdToIndex(iter->mArg); + effect.mAttribute = ESM::Attribute::refIdToIndex(effectKey.mArg); effect.mRange = 0; effect.mArea = 0; @@ -231,7 +250,7 @@ const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co if (iter->mName != toFind.mName || iter->mScript != toFind.mScript || iter->mData.mWeight != toFind.mData.mWeight || iter->mData.mValue != toFind.mData.mValue - || iter->mData.mAutoCalc != toFind.mData.mAutoCalc) + || iter->mData.mFlags != toFind.mData.mFlags) continue; // Don't choose an ID that came from the content files, would have unintended side effects @@ -241,15 +260,15 @@ const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co bool mismatch = false; - for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) + for (size_t i = 0; i < iter->mEffects.mList.size(); ++i) { - const ESM::ENAMstruct& first = iter->mEffects.mList[i]; + const ESM::IndexedENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = mEffects[i]; - if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange - || first.mSkill != second.mSkill || first.mAttribute != second.mAttribute - || first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax - || first.mDuration != second.mDuration) + if (first.mData.mEffectID != second.mEffectID || first.mData.mArea != second.mArea + || first.mData.mRange != second.mRange || first.mData.mSkill != second.mSkill + || first.mData.mAttribute != second.mAttribute || first.mData.mMagnMin != second.mMagnMin + || first.mData.mMagnMax != second.mMagnMax || first.mData.mDuration != second.mDuration) { mismatch = true; break; @@ -270,7 +289,7 @@ void MWMechanics::Alchemy::removeIngredients() { iter->getContainerStore()->remove(*iter, 1); - if (iter->getRefData().getCount() < 1) + if (iter->getCellRef().getCount() < 1) *iter = MWWorld::Ptr(); } @@ -291,7 +310,7 @@ void MWMechanics::Alchemy::addPotion(const std::string& name) newRecord.mData.mWeight /= countIngredients(); newRecord.mData.mValue = mValue; - newRecord.mData.mAutoCalc = 0; + newRecord.mData.mFlags = 0; newRecord.mRecordFlags = 0; newRecord.mName = name; @@ -305,7 +324,7 @@ void MWMechanics::Alchemy::addPotion(const std::string& name) newRecord.mModel = "m\\misc_potion_" + std::string(meshes[index]) + "_01.nif"; newRecord.mIcon = "m\\tx_potion_" + std::string(meshes[index]) + "_01.dds"; - newRecord.mEffects.mList = mEffects; + newRecord.mEffects.populate(mEffects); const ESM::Potion* record = getRecord(newRecord); if (!record) @@ -316,7 +335,7 @@ void MWMechanics::Alchemy::addPotion(const std::string& name) void MWMechanics::Alchemy::increaseSkill() { - mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, 0); + mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, ESM::Skill::Alchemy_CreatePotion); } float MWMechanics::Alchemy::getAlchemyFactor() const @@ -350,7 +369,7 @@ int MWMechanics::Alchemy::countPotionsToBrew() const for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) { - int count = iter->getRefData().getCount(); + int count = iter->getCellRef().getCount(); if ((count > 0 && count < toBrew) || toBrew < 0) toBrew = count; } @@ -368,6 +387,8 @@ void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc) mTools.resize(4); + std::vector prevTools(mTools); + std::fill(mTools.begin(), mTools.end(), MWWorld::Ptr()); mEffects.clear(); @@ -384,6 +405,12 @@ void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc) if (type < 0 || type >= static_cast(mTools.size())) throw std::runtime_error("invalid apparatus type"); + if (prevTools[type] == *iter) + mTools[type] = *iter; // prefer the previous tool if still in the container + + if (!mTools[type].isEmpty() && !prevTools[type].isEmpty() && mTools[type] == prevTools[type]) + continue; + if (!mTools[type].isEmpty()) if (ref->mBase->mData.mQuality <= mTools[type].get()->mBase->mData.mQuality) continue; @@ -415,7 +442,6 @@ MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::endIngredients( void MWMechanics::Alchemy::clear() { mAlchemist = MWWorld::Ptr(); - mTools.clear(); mIngredients.clear(); mEffects.clear(); setPotionName(""); @@ -452,15 +478,33 @@ int MWMechanics::Alchemy::addIngredient(const MWWorld::Ptr& ingredient) return slot; } -void MWMechanics::Alchemy::removeIngredient(int index) +void MWMechanics::Alchemy::removeIngredient(size_t index) { - if (index >= 0 && index < static_cast(mIngredients.size())) + if (index < mIngredients.size()) { mIngredients[index] = MWWorld::Ptr(); updateEffects(); } } +void MWMechanics::Alchemy::addApparatus(const MWWorld::Ptr& apparatus) +{ + int32_t slot = apparatus.get()->mBase->mData.mType; + + mTools[slot] = apparatus; + + updateEffects(); +} + +void MWMechanics::Alchemy::removeApparatus(size_t index) +{ + if (index < mTools.size()) + { + mTools[index] = MWWorld::Ptr(); + updateEffects(); + } +} + MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::beginEffects() const { return mEffects.begin(); @@ -510,6 +554,8 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create(const std::string& nam if (readyStatus != Result_Success) return readyStatus; + MWBase::Environment::get().getWorld()->breakInvisibility(mAlchemist); + Result result = Result_RandomFailure; int brewedCount = 0; for (int i = 0; i < count; ++i) @@ -551,7 +597,7 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle() std::string MWMechanics::Alchemy::suggestPotionName() { - std::set effects = listEffects(); + std::vector effects = listEffects(); if (effects.empty()) return {}; @@ -568,11 +614,11 @@ std::vector MWMechanics::Alchemy::effectsDescription(const MWWorld: const static auto fWortChanceValue = store->get().find("fWortChanceValue")->mValue.getFloat(); const auto& data = item->mData; - for (auto i = 0; i < 4; ++i) + for (size_t i = 0; i < sNumEffects; ++i) { const auto effectID = data.mEffectID[i]; - if (alchemySkill < fWortChanceValue * (i + 1)) + if (alchemySkill < fWortChanceValue * static_cast(i + 1)) break; if (effectID != -1) diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index ab6225e544..373ca8b887 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -1,7 +1,6 @@ #ifndef GAME_MWMECHANICS_ALCHEMY_H #define GAME_MWMECHANICS_ALCHEMY_H -#include #include #include @@ -110,7 +109,7 @@ namespace MWMechanics void setPotionName(const std::string& name); ///< Set name of potion to create - std::set listEffects() const; + std::vector listEffects() const; ///< List all effects shared by at least two ingredients. int addIngredient(const MWWorld::Ptr& ingredient); @@ -119,9 +118,15 @@ namespace MWMechanics /// \return Slot index or -1, if adding failed because of no free slot or the ingredient type being /// listed already. - void removeIngredient(int index); + void addApparatus(const MWWorld::Ptr& apparatus); + ///< Add apparatus into the appropriate slot. + + void removeIngredient(size_t index); ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). + void removeApparatus(size_t index); + ///< Remove apparatus from slot. + std::string suggestPotionName(); ///< Suggest a name for the potion, based on the current effects diff --git a/apps/openmw/mwmechanics/attacktype.hpp b/apps/openmw/mwmechanics/attacktype.hpp new file mode 100644 index 0000000000..3824f5bbe7 --- /dev/null +++ b/apps/openmw/mwmechanics/attacktype.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_MWMECHANICS_ATTACKTYPE_H +#define OPENMW_MWMECHANICS_ATTACKTYPE_H + +namespace MWMechanics +{ + enum class AttackType + { + NoAttack, + Any, + Chop, + Slash, + Thrust + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index 6581aacdd2..5bab25fbe5 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -74,7 +74,8 @@ namespace MWMechanics ESM::RefId school; float skillTerm; calcWeakestSchool(&spell, actorSkills, school, skillTerm); - assert(!school.empty()); + if (school.empty()) + continue; SchoolCaps& cap = schoolCaps[school]; if (cap.mReachedLimit && spellCost <= cap.mMinCost) @@ -220,7 +221,7 @@ namespace MWMechanics for (const auto& spellEffect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(spellEffect.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(spellEffect.mData.mEffectID); static const int iAutoSpellAttSkillMin = MWBase::Environment::get() .getESMStore() ->get() @@ -229,7 +230,7 @@ namespace MWMechanics if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) { - ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mSkill); + ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill); auto found = actorSkills.find(skill); if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; @@ -237,7 +238,7 @@ namespace MWMechanics if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) { - ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute); + ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); auto found = actorAttributes.find(attribute); if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; @@ -252,22 +253,22 @@ namespace MWMechanics { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float minChance = std::numeric_limits::max(); - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); int minMagn = 1; int maxMagn = 1; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { - minMagn = effect.mMagnMin; - maxMagn = effect.mMagnMax; + minMagn = effect.mData.mMagnMin; + maxMagn = effect.mData.mMagnMax; } int duration = 0; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - duration = effect.mDuration; + duration = effect.mData.mDuration; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) duration = std::max(1, duration); @@ -280,10 +281,10 @@ namespace MWMechanics float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); x *= 0.1 * magicEffect->mData.mBaseCost; x *= 1 + duration; - x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; + x += 0.05 * std::max(1, effect.mData.mArea) * magicEffect->mData.mBaseCost; x *= fEffectCostMult; - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; float s = 0.f; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 823dd0f598..c0359881b6 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -20,6 +20,7 @@ #include "character.hpp" #include +#include #include #include @@ -35,6 +36,7 @@ #include "../mwrender/animation.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -269,7 +271,7 @@ namespace case CharState_IdleSwim: return Priority_SwimIdle; case CharState_IdleSneak: - priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + priority[MWRender::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; [[fallthrough]]; default: return priority; @@ -353,6 +355,7 @@ namespace MWMechanics { clearStateAnimation(mCurrentMovement); mMovementState = CharState_None; + mMovementAnimationHasMovement = false; } void CharacterController::resetCurrentIdleState() @@ -442,8 +445,8 @@ namespace MWMechanics { mHitState = CharState_Block; priority = Priority_Hit; - priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; - priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + priority[MWRender::BoneGroup_LeftArm] = Priority_Block; + priority[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; startKey = "block start"; stopKey = "block stop"; } @@ -480,8 +483,8 @@ namespace MWMechanics return; } - mAnimation->play( - mCurrentHit, priority, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul); + playBlendedAnimation(mCurrentHit, priority, MWRender::BlendMask_All, true, 1, startKey, stopKey, 0.0f, + std::numeric_limits::max()); } void CharacterController::refreshJumpAnims(JumpingState jump, bool force) @@ -500,7 +503,7 @@ namespace MWMechanics std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); std::string jumpAnimName = "jump"; jumpAnimName += weapShortGroup; - MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; + MWRender::Animation::BlendMask jumpmask = MWRender::BlendMask_All; if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName)) jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); @@ -518,10 +521,10 @@ namespace MWMechanics mCurrentJump = jumpAnimName; if (mJumpState == JumpState_InAir) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", - "stop", 0.f, ~0ul); + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, + startAtLoop ? "loop start" : "start", "stop", 0.f, std::numeric_limits::max()); else if (mJumpState == JumpState_Landing) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); } bool CharacterController::onOpen() const @@ -537,8 +540,8 @@ namespace MWMechanics if (mAnimation->isPlaying("containerclose")) return false; - mAnimation->play("containeropen", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", 0.f, 0); + mAnimation->play( + "containeropen", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); if (mAnimation->isPlaying("containeropen")) return false; } @@ -558,8 +561,8 @@ namespace MWMechanics if (animPlaying) startPoint = 1.f - complete; - mAnimation->play("containerclose", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", startPoint, 0); + mAnimation->play("containerclose", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", + startPoint, 0); } } @@ -598,7 +601,7 @@ namespace MWMechanics if (!isRealWeapon(mWeaponType)) { if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; return baseGroupName; } @@ -617,13 +620,13 @@ namespace MWMechanics // Special case for crossbows - we should apply 1h animations a fallback only for lower body if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; if (!mAnimation->hasAnimation(groupName)) { groupName = baseGroupName; if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; } return groupName; @@ -656,7 +659,7 @@ namespace MWMechanics } } - MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All; + MWRender::Animation::BlendMask movemask = MWRender::BlendMask_All; std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); @@ -682,7 +685,7 @@ namespace MWMechanics if (!mAnimation->hasAnimation(weapMovementAnimName)) weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); - movementAnimName = weapMovementAnimName; + movementAnimName = std::move(weapMovementAnimName); } if (!mAnimation->hasAnimation(movementAnimName)) @@ -705,10 +708,10 @@ namespace MWMechanics if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement) mAnimation->getInfo(mCurrentMovement, &startpoint); - mMovementAnimationControlled = true; + mMovementAnimationHasMovement = true; clearStateAnimation(mCurrentMovement); - mCurrentMovement = movementAnimName; + mCurrentMovement = std::move(movementAnimName); // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. @@ -743,12 +746,12 @@ namespace MWMechanics bool sneaking = mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack || mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight; mMovementAnimSpeed = (sneaking ? 33.5452f : (isRunning() ? 222.857f : 154.064f)); - mMovementAnimationControlled = false; + mMovementAnimationHasMovement = false; } } - mAnimation->play( - mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); + playBlendedAnimation(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, + std::numeric_limits::max(), true); } void CharacterController::refreshIdleAnims(CharacterState idle, bool force) @@ -776,7 +779,7 @@ namespace MWMechanics } MWRender::Animation::AnimPriority priority = getIdlePriority(mIdleState); - size_t numLoops = std::numeric_limits::max(); + size_t numLoops = std::numeric_limits::max(); // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to // "idle"+weapon or "idle". @@ -796,7 +799,7 @@ namespace MWMechanics weapIdleGroup += weapShortGroup; if (!mAnimation->hasAnimation(weapIdleGroup)) weapIdleGroup = fallbackShortWeaponGroup(idleGroup); - idleGroup = weapIdleGroup; + idleGroup = std::move(weapIdleGroup); // play until the Loop Stop key 2 to 5 times, then play until the Stop key // this replicates original engine behavior for the "Idle1h" 1st-person animation @@ -818,16 +821,16 @@ namespace MWMechanics mAnimation->getInfo(mCurrentIdle, &startPoint); clearStateAnimation(mCurrentIdle); - mCurrentIdle = idleGroup; - mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", - startPoint, numLoops, true); + mCurrentIdle = std::move(idleGroup); + playBlendedAnimation( + mCurrentIdle, priority, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } void CharacterController::refreshCurrentAnims( CharacterState idle, CharacterState movement, JumpingState jump, bool force) { - // If the current animation is persistent, do not touch it - if (isPersistentAnimPlaying()) + // If the current animation is scripted, do not touch it + if (isScriptedAnimPlaying()) return; refreshHitRecoilAnims(); @@ -852,10 +855,9 @@ namespace MWMechanics resetCurrentHitState(); resetCurrentIdleState(); resetCurrentJumpState(); - mMovementAnimationControlled = true; - mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, false, 1.0f, "start", - "stop", startpoint, 0); + playBlendedAnimation( + mCurrentDeath, Priority_Death, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startpoint, 0); } CharacterState CharacterController::chooseRandomDeathState() const @@ -882,7 +884,7 @@ namespace MWMechanics mDeathState = chooseRandomDeathState(); // Do not interrupt scripted animation by death - if (isPersistentAnimPlaying()) + if (isScriptedAnimPlaying()) return; playDeath(startpoint, mDeathState); @@ -997,6 +999,8 @@ namespace MWMechanics { std::string_view evt = key->second; + MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second); + if (evt.substr(0, 7) == "sound: ") { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); @@ -1056,17 +1060,23 @@ namespace MWMechanics std::string_view action = evt.substr(groupname.size() + 2); if (action == "equip attach") { - if (groupname == "shield") - mAnimation->showCarriedLeft(true); - else - mAnimation->showWeapons(true); + if (mUpperBodyState == UpperBodyState::Equipping) + { + if (groupname == "shield") + mAnimation->showCarriedLeft(true); + else + mAnimation->showWeapons(true); + } } else if (action == "unequip detach") { - if (groupname == "shield") - mAnimation->showCarriedLeft(false); - else - mAnimation->showWeapons(false); + if (mUpperBodyState == UpperBodyState::Unequipping) + { + if (groupname == "shield") + mAnimation->showCarriedLeft(false); + else + mAnimation->showWeapons(false); + } } else if (action == "chop hit" || action == "slash hit" || action == "thrust hit" || action == "hit") { @@ -1151,8 +1161,8 @@ namespace MWMechanics else if (groupname == "spellcast" && action == mAttackType + " release") { if (mCanCast) - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); - mCastingManualSpell = false; + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingScriptedSpell); + mCastingScriptedSpell = false; mCanCast = false; } else if (groupname == "containeropen" && action == "loot") @@ -1166,9 +1176,14 @@ namespace MWMechanics void CharacterController::updateIdleStormState(bool inwater) const { - if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperBodyState::None || inwater) + if (!mAnimation->hasAnimation("idlestorm")) + return; + + bool animPlaying = mAnimation->isPlaying("idlestorm"); + if (mUpperBodyState != UpperBodyState::None || inwater) { - mAnimation->disable("idlestorm"); + if (animPlaying) + mAnimation->disable("idlestorm"); return; } @@ -1181,10 +1196,11 @@ namespace MWMechanics characterDirection.normalize(); if (stormDirection * characterDirection < -0.5f) { - if (!mAnimation->isPlaying("idlestorm")) + if (!animPlaying) { - int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm; - mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul); + int mask = MWRender::BlendMask_Torso | MWRender::BlendMask_RightArm; + playBlendedAnimation("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, + std::numeric_limits::max(), true); } else { @@ -1194,7 +1210,7 @@ namespace MWMechanics } } - if (mAnimation->isPlaying("idlestorm")) + if (animPlaying) { mAnimation->setLoopingEnabled("idlestorm", false); } @@ -1243,6 +1259,10 @@ namespace MWMechanics bool CharacterController::updateWeaponState() { + // If the current animation is scripted, we can't do anything here. + if (isScriptedAnimPlaying()) + return false; + const auto world = MWBase::Environment::get().getWorld(); auto& prng = world->getPrng(); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); @@ -1312,8 +1332,8 @@ namespace MWMechanics if (mAnimation->isPlaying("shield")) mAnimation->disable("shield"); - mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, false, 1.0f, "start", - "stop", 0.0f, std::numeric_limits::max(), true); + playBlendedAnimation("torch", Priority_Torch, MWRender::BlendMask_LeftArm, false, 1.0f, "start", "stop", + 0.0f, std::numeric_limits::max(), true); } else if (mAnimation->isPlaying("torch")) { @@ -1321,10 +1341,14 @@ namespace MWMechanics } } - // For biped actors, blend weapon animations with lower body animations with higher priority - MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); + MWRender::Animation::AnimPriority priorityWeapon(Priority_Default); if (cls.isBipedal(mPtr)) - priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + { + // For bipeds, blend weapon animations with lower body animations with higher priority + // For non-bipeds, movement takes priority + priorityWeapon = Priority_Weapon; + priorityWeapon[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + } bool forcestateupdate = false; @@ -1346,7 +1370,7 @@ namespace MWMechanics if (!isKnockedOut() && !isKnockedDown() && !isRecovery()) { std::string weapgroup; - if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell) && weaptype != mWeaponType + if (((!isWerewolf && cls.isBipedal(mPtr)) || mWeaponType != ESM::Weapon::Spell) && weaptype != mWeaponType && mUpperBodyState <= UpperBodyState::AttackWindUp && mUpperBodyState != UpperBodyState::Unequipping && !isStillWeapon) { @@ -1355,19 +1379,20 @@ namespace MWMechanics { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); - int unequipMask = MWRender::Animation::BlendMask_All; + int unequipMask = MWRender::BlendMask_All; bool useShieldAnims = mAnimation->useShieldAnimations(); if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) { - unequipMask = unequipMask | ~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, 1.0f, + unequipMask = unequipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, "unequip start", "unequip stop", 0.0f, 0); } else if (mWeaponType == ESM::Weapon::HandToHand) mAnimation->showCarriedLeft(false); - mAnimation->play( + mAnimation->disable(weapgroup); + playBlendedAnimation( weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperBodyState::Unequipping; @@ -1413,16 +1438,19 @@ namespace MWMechanics if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); - int equipMask = MWRender::Animation::BlendMask_All; + int equipMask = MWRender::BlendMask_All; if (useShieldAnims && weaptype != ESM::Weapon::Spell) { - equipMask = equipMask | ~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); + equipMask = equipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, + "equip start", "equip stop", 0.0f, 0); } - mAnimation->play( - weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); + if (weaptype != ESM::Weapon::Spell || cls.isBipedal(mPtr)) + { + playBlendedAnimation(weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", + "equip stop", 0.0f, 0); + } mUpperBodyState = UpperBodyState::Equipping; // If we do not have the "equip attach" key, show weapon manually. @@ -1476,10 +1504,6 @@ namespace MWMechanics sndMgr->stopSound3D(mPtr, wolfRun); } - // Combat for actors with persistent animations obviously will be buggy - if (isPersistentAnimPlaying()) - return forcestateupdate; - float complete = 0.f; bool animPlaying = false; ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; @@ -1516,9 +1540,9 @@ namespace MWMechanics bool isMagicItem = false; // Play hand VFX and allow castSpell use (assuming an animation is going to be played) if - // spellcasting is successful. Manual spellcasting bypasses restrictions. + // spellcasting is successful. Scripted spellcasting bypasses restrictions. MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success; - if (!mCastingManualSpell) + if (!mCastingScriptedSpell) spellCastResult = world->startSpellCast(mPtr); mCanCast = spellCastResult == MWWorld::SpellCastState::Success; @@ -1548,9 +1572,9 @@ namespace MWMechanics else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed) { world->breakInvisibility(mPtr); - MWMechanics::CastSpell cast(mPtr, {}, false, mCastingManualSpell); + MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell); - const std::vector* effects{ nullptr }; + const std::vector* effects{ nullptr }; const MWWorld::ESMStore& store = world->getStore(); if (isMagicItem) { @@ -1564,66 +1588,64 @@ namespace MWMechanics effects = &spell->mEffects.mList; cast.playSpellCastingEffects(spell); } - if (mCanCast) + if (!effects->empty()) { - const ESM::MagicEffect* effect = store.get().find( - effects->back().mEffectID); // use last effect of list for color of VFX_Hands - - const ESM::Static* castStatic - = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); - - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - - if (!effects->empty()) + if (mCanCast) { + const ESM::MagicEffect* effect = store.get().find( + effects->back().mData.mEffectID); // use last effect of list for color of VFX_Hands + + const ESM::Static* castStatic + = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); + + const VFS::Path::Normalized castStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(castStatic->mModel)); + if (mAnimation->getNode("Bip01 L Hand")) mAnimation->addEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), -1, false, - "Bip01 L Hand", effect->mParticle); + castStaticModel.value(), "", false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) mAnimation->addEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), -1, false, - "Bip01 R Hand", effect->mParticle); + castStaticModel.value(), "", false, "Bip01 R Hand", effect->mParticle); } - } + // first effect used for casting animation + const ESM::ENAMstruct& firstEffect = effects->front().mData; - const ESM::ENAMstruct& firstEffect = effects->at(0); // first effect used for casting animation - - std::string startKey; - std::string stopKey; - if (isRandomAttackAnimation(mCurrentWeapon)) - { - startKey = "start"; - stopKey = "stop"; - if (mCanCast) - world->castSpell( - mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately - mCastingManualSpell = false; - mCanCast = false; - } - else - { - switch (firstEffect.mRange) + std::string startKey; + std::string stopKey; + if (isRandomAttackAnimation(mCurrentWeapon)) { - case 0: - mAttackType = "self"; - break; - case 1: - mAttackType = "touch"; - break; - case 2: - mAttackType = "target"; - break; + startKey = "start"; + stopKey = "stop"; + if (mCanCast) + world->castSpell(mPtr, + mCastingScriptedSpell); // No "release" text key to use, so cast immediately + mCastingScriptedSpell = false; + mCanCast = false; } + else + { + switch (firstEffect.mRange) + { + case 0: + mAttackType = "self"; + break; + case 1: + mAttackType = "touch"; + break; + case 2: + mAttackType = "target"; + break; + } - startKey = mAttackType + " start"; - stopKey = mAttackType + " stop"; + startKey = mAttackType + " start"; + stopKey = mAttackType + " stop"; + } + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, 1, + startKey, stopKey, 0.0f, 0); + mUpperBodyState = UpperBodyState::Casting; } - - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, 1, - startKey, stopKey, 0.0f, 0); - mUpperBodyState = UpperBodyState::Casting; } else { @@ -1635,6 +1657,10 @@ namespace MWMechanics std::string startKey = "start"; std::string stopKey = "stop"; + MWBase::LuaManager::ActorControls* actorControls + = MWBase::Environment::get().getLuaManager()->getActorControls(mPtr); + const bool aiInactive + = actorControls->mDisableAI || !MWBase::Environment::get().getMechanicsManager()->isAIActive(); if (mWeaponType != ESM::Weapon::PickProbe && !isRandomAttackAnimation(mCurrentWeapon)) { if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) @@ -1658,6 +1684,13 @@ namespace MWMechanics mAttackType = getMovementBasedAttackType(); } } + else if (aiInactive) + { + mAttackType = getDesiredAttackType(); + if (mAttackType == "") + mAttackType = getRandomAttackType(); + } + // else if (mPtr != getPlayer()) use mAttackType set by AiCombat startKey = mAttackType + ' ' + startKey; stopKey = mAttackType + " max attack"; @@ -1672,8 +1705,8 @@ namespace MWMechanics mAttackVictim = MWWorld::Ptr(); mAttackHitPos = osg::Vec3f(); - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, - weapSpeed, startKey, stopKey, 0.0f, 0); + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, + startKey, stopKey, 0.0f, 0); } } @@ -1746,7 +1779,7 @@ namespace MWMechanics } mAnimation->disable(mCurrentWeapon); - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, weapSpeed, + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, mAttackType + " max attack", mAttackType + ' ' + hit, startPoint, 0); } @@ -1772,11 +1805,7 @@ namespace MWMechanics if (animPlaying) mAnimation->disable(mCurrentWeapon); - MWRender::Animation::AnimPriority priorityFollow(priorityWeapon); - // Follow animations have lower priority than movement for non-biped creatures, logic be damned - if (!cls.isBipedal(mPtr)) - priorityFollow = Priority_Default; - mAnimation->play(mCurrentWeapon, priorityFollow, MWRender::Animation::BlendMask_All, false, weapSpeed, + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, mAttackType + ' ' + start, mAttackType + ' ' + stop, 0.0f, 0); mUpperBodyState = UpperBodyState::AttackEnd; @@ -1847,23 +1876,70 @@ namespace MWMechanics void CharacterController::updateAnimQueue() { - if (mAnimQueue.size() > 1) + if (mAnimQueue.empty()) + return; + + if (!mAnimation->isPlaying(mAnimQueue.front().mGroup)) { - if (mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) + // Playing animations through mwscript is weird. If an animation is + // a looping animation (idle or other cyclical animations), then they + // will end as expected. However, if they are non-looping animations, they + // will stick around forever or until another animation appears in the queue. + bool shouldPlayOrRestart = mAnimQueue.size() > 1; + if (shouldPlayOrRestart || !mAnimQueue.front().mScripted + || (mAnimQueue.front().mLoopCount == 0 && mAnimQueue.front().mLooping)) { + mAnimation->setPlayScriptedOnly(false); mAnimation->disable(mAnimQueue.front().mGroup); mAnimQueue.pop_front(); - - bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle"); - mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); + shouldPlayOrRestart = true; } + else + // A non-looping animation will stick around forever, so only restart if the animation + // actually was removed for some reason. + shouldPlayOrRestart = !mAnimation->getInfo(mAnimQueue.front().mGroup) + && mAnimation->hasAnimation(mAnimQueue.front().mGroup); + + if (shouldPlayOrRestart) + { + // Move on to the remaining items of the queue + playAnimQueue(); + } + } + else + { + float complete; + size_t loopcount; + mAnimation->getInfo(mAnimQueue.front().mGroup, &complete, nullptr, &loopcount); + mAnimQueue.front().mLoopCount = loopcount; + mAnimQueue.front().mTime = complete; } if (!mAnimQueue.empty()) mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); } + void CharacterController::playAnimQueue(bool loopStart) + { + if (!mAnimQueue.empty()) + { + clearStateAnimation(mCurrentIdle); + mIdleState = CharState_SpecialIdle; + auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default; + mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted); + if (mAnimQueue.front().mScripted) + mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); + else + playBlendedAnimation(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); + } + } + void CharacterController::update(float duration) { MWBase::World* world = MWBase::Environment::get().getWorld(); @@ -1884,7 +1960,7 @@ namespace MWMechanics { const ESM::NPC* npc = mPtr.get()->mBase; const ESM::Race* race = world->getStore().get().find(npc->mRace); - float weight = npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; + float weight = npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; scale *= weight; } @@ -1978,7 +2054,8 @@ namespace MWMechanics float effectiveRotation = rot.z(); bool canMove = cls.getMaxSpeed(mPtr) > 0; const bool turnToMovementDirection = Settings::game().mTurnToMovementDirection; - if (!turnToMovementDirection || isFirstPersonPlayer) + const bool isBiped = mPtr.getClass().isBipedal(mPtr); + if (!isBiped || !turnToMovementDirection || isFirstPersonPlayer) { movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; stats.setSideMovementAngle(0); @@ -2018,7 +2095,7 @@ namespace MWMechanics vec.x() *= speed; vec.y() *= speed; - if (isKnockedOut() || isKnockedDown() || isRecovery()) + if (isKnockedOut() || isKnockedDown() || isRecovery() || isScriptedAnimPlaying()) vec = osg::Vec3f(); CharacterState movestate = CharState_None; @@ -2036,7 +2113,7 @@ namespace MWMechanics mSecondsOfSwimming += duration; while (mSecondsOfSwimming > 1) { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1); + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_SwimOneSecond); mSecondsOfSwimming -= 1; } } @@ -2045,7 +2122,7 @@ namespace MWMechanics mSecondsOfRunning += duration; while (mSecondsOfRunning > 1) { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0); + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_RunOneSecond); mSecondsOfRunning -= 1; } } @@ -2092,6 +2169,15 @@ namespace MWMechanics bool wasInJump = mInJump; mInJump = false; + const float jumpHeight = cls.getJump(mPtr); + if (jumpHeight <= 0.f || sneak || inwater || flying || !solid) + { + vec.z() = 0.f; + // Following code might assign some vertical movement regardless, need to reset this manually + // This is used for jumping detection + movementSettings.mPosition[2] = 0; + } + if (!inwater && !flying && solid) { // In the air (either getting up —ascending part of jump— or falling). @@ -2110,20 +2196,16 @@ namespace MWMechanics vec.z() = 0.0f; } // Started a jump. - else if (mJumpState != JumpState_InAir && vec.z() > 0.f && !sneak) + else if (mJumpState != JumpState_InAir && vec.z() > 0.f) { - float z = cls.getJump(mPtr); - if (z > 0.f) + mInJump = true; + if (vec.x() == 0 && vec.y() == 0) + vec.z() = jumpHeight; + else { - mInJump = true; - if (vec.x() == 0 && vec.y() == 0) - vec.z() = z; - else - { - osg::Vec3f lat(vec.x(), vec.y(), 0.0f); - lat.normalize(); - vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f; - } + osg::Vec3f lat(vec.x(), vec.y(), 0.0f); + lat.normalize(); + vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * jumpHeight * 0.707f; } } } @@ -2163,7 +2245,7 @@ namespace MWMechanics { // report acrobatics progression if (isPlayer) - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1); + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Fall); } } @@ -2185,8 +2267,6 @@ namespace MWMechanics if (mAnimation->isPlaying(mCurrentJump)) jumpstate = JumpState_Landing; - vec.x() *= scale; - vec.y() *= scale; vec.z() = 0.0f; if (movementSettings.mIsStrafing) @@ -2219,7 +2299,7 @@ namespace MWMechanics // It seems only bipedal actors use turning animations. // Also do not use turning animations in the first-person view and when sneaking. - if (!sneak && !isFirstPersonPlayer && mPtr.getClass().isBipedal(mPtr)) + if (!sneak && !isFirstPersonPlayer && isBiped) { if (effectiveRotation > rotationThreshold) movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; @@ -2229,7 +2309,7 @@ namespace MWMechanics } } - if (turnToMovementDirection && !isFirstPersonPlayer + if (turnToMovementDirection && !isFirstPersonPlayer && isBiped && (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward || movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack)) { @@ -2263,7 +2343,7 @@ namespace MWMechanics } else { - if (mPtr.getClass().isBipedal(mPtr)) + if (isBiped) { if (mTurnAnimationThreshold > 0) mTurnAnimationThreshold -= duration; @@ -2286,17 +2366,16 @@ namespace MWMechanics jumpstate = JumpState_None; } - if (mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle)) - { - if (inwater) - idlestate = CharState_IdleSwim; - else if (sneak && !mInJump) - idlestate = CharState_IdleSneak; - else - idlestate = CharState_Idle; - } + updateAnimQueue(); + if (!mAnimQueue.empty()) + idlestate = CharState_SpecialIdle; + else if (sneak && !mInJump) + idlestate = CharState_IdleSneak; else - updateAnimQueue(); + idlestate = CharState_Idle; + + if (inwater) + idlestate = CharState_IdleSwim; if (!mSkipAnim) { @@ -2304,9 +2383,6 @@ namespace MWMechanics updateIdleStormState(inwater); } - if (mInJump) - mMovementAnimationControlled = false; - if (isTurning()) { // Adjust animation speed from 1.0 to 1.5 multiplier @@ -2323,7 +2399,8 @@ namespace MWMechanics const float speedMult = speed / mMovementAnimSpeed; mAnimation->adjustSpeedMult(mCurrentMovement, std::min(maxSpeedMult, speedMult)); // Make sure the actual speed is the "expected" speed even though the animation is slower - scale *= std::max(1.f, speedMult / maxSpeedMult); + if (isMovementAnimationControlled()) + scale *= std::max(1.f, speedMult / maxSpeedMult); } if (!mSkipAnim) @@ -2342,20 +2419,17 @@ namespace MWMechanics } } - if (!mMovementAnimationControlled) - world->queueMovement(mPtr, vec); + updateHeadTracking(duration); } movement = vec; movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; + + // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicsSystem will + // actually handle it in this frame due to the fixed minimum timestep used for the physics update. It will + // be reset in PhysicsSystem::move once the jump is handled. if (movement.z() == 0.f) movementSettings.mPosition[2] = 0; - // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicSystem will - // actually handle it in this frame due to the fixed minimum timestep used for the physics update. It will - // be reset in PhysicSystem::move once the jump is handled. - - if (!mSkipAnim) - updateHeadTracking(duration); } else if (cls.getCreatureStats(mPtr).isDead()) { @@ -2369,50 +2443,63 @@ namespace MWMechanics } } - bool isPersist = isPersistentAnimPlaying(); - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); - if (duration > 0.0f) - moved /= duration; - else - moved = osg::Vec3f(0.f, 0.f, 0.f); + osg::Vec3f movementFromAnimation + = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration); - moved.x() *= scale; - moved.y() *= scale; - - // Ensure we're moving in generally the right direction... - if (speed > 0.f && moved != osg::Vec3f()) + if (mPtr.getClass().isActor() && !isScriptedAnimPlaying()) { - float l = moved.length(); - if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 - || std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2 - || std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2) + if (isMovementAnimationControlled()) { - moved = movement; - // For some creatures getSpeed doesn't work, so we adjust speed to the animation. - // TODO: Fix Creature::getSpeed. - float newLength = moved.length(); - if (newLength > 0 && !cls.isNpc()) - moved *= (l / newLength); - } - } + if (duration != 0.f && movementFromAnimation != osg::Vec3f()) + { + movementFromAnimation /= duration; - if (mFloatToSurface && cls.isActor()) - { - if (cls.getCreatureStats(mPtr).isDead() - || (!godmode - && cls.getCreatureStats(mPtr) - .getMagicEffects() - .getOrDefault(ESM::MagicEffect::Paralyze) - .getModifier() - > 0)) + // Ensure we're moving in the right general direction. + // In vanilla, all horizontal movement is taken from animations, even when moving diagonally (which + // doesn't have a corresponding animation). So to achieve diagonal movement, we have to rotate the + // movement taken from the animation to the intended direction. + // + // Note that while a complete movement animation cycle will have a well defined direction, no + // individual frame will, and therefore we have to determine the direction based on the currently + // playing cycle instead. + if (speed > 0.f) + { + float animMovementAngle = getAnimationMovementDirection(); + float targetMovementAngle = std::atan2(-movement.x(), movement.y()); + float diff = targetMovementAngle - animMovementAngle; + movementFromAnimation = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * movementFromAnimation; + } + + movement = movementFromAnimation; + } + else + { + movement = osg::Vec3f(); + } + } + else if (mSkipAnim) { - moved.z() = 1.0; + movement = osg::Vec3f(); } - } - // Update movement - if (mMovementAnimationControlled && mPtr.getClass().isActor()) - world->queueMovement(mPtr, moved); + if (mFloatToSurface && world->isSwimming(mPtr)) + { + if (cls.getCreatureStats(mPtr).isDead() + || (!godmode + && cls.getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Paralyze) + .getModifier() + > 0)) + { + movement.z() = 1.0; + } + } + + movement.x() *= scale; + movement.y() *= scale; + world->queueMovement(mPtr, movement); + } mSkipAnim = false; @@ -2426,7 +2513,8 @@ namespace MWMechanics state.mScriptedAnims.clear(); for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { - if (!iter->mPersist) + // TODO: Probably want to presist lua animations too + if (!iter->mScripted) continue; ESM::AnimationState::ScriptedAnimation anim; @@ -2434,10 +2522,11 @@ namespace MWMechanics if (iter == mAnimQueue.begin()) { - anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup); float complete; - mAnimation->getInfo(anim.mGroup, &complete, nullptr); + size_t loopcount; + mAnimation->getInfo(anim.mGroup, &complete, nullptr, &loopcount); anim.mTime = complete; + anim.mLoopCount = loopcount; } else { @@ -2461,47 +2550,58 @@ namespace MWMechanics { AnimationQueueEntry entry; entry.mGroup = iter->mGroup; - entry.mLoopCount = iter->mLoopCount; - entry.mPersist = true; + entry.mLoopCount + = static_cast(std::min(iter->mLoopCount, std::numeric_limits::max())); + entry.mLooping = mAnimation->isLoopingAnimation(entry.mGroup); + entry.mScripted = true; + entry.mStartKey = "start"; + entry.mStopKey = "stop"; + entry.mSpeed = 1.f; + entry.mTime = iter->mTime; + if (iter->mAbsolute) + { + float start = mAnimation->getTextKeyTime(iter->mGroup + ": start"); + float stop = mAnimation->getTextKeyTime(iter->mGroup + ": stop"); + float time = std::clamp(iter->mTime, start, stop); + entry.mTime = (time - start) / (stop - start); + } mAnimQueue.push_back(entry); } - const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front(); - float complete = anim.mTime; - if (anim.mAbsolute) - { - float start = mAnimation->getTextKeyTime(anim.mGroup + ": start"); - float stop = mAnimation->getTextKeyTime(anim.mGroup + ": stop"); - float time = std::clamp(anim.mTime, start, stop); - complete = (time - start) / (stop - start); - } - - clearStateAnimation(mCurrentIdle); - mIdleState = CharState_SpecialIdle; - - bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle"); - mAnimation->play(anim.mGroup, Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", - "stop", complete, anim.mLoopCount, loopfallback); + playAnimQueue(); } } - bool CharacterController::playGroup(std::string_view groupname, int mode, int count, bool persist) + void CharacterController::playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, + int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop, + float startpoint, uint32_t loops, bool loopfallback) const + { + if (mLuaAnimations) + MWBase::Environment::get().getLuaManager()->playAnimation(mPtr, groupname, priority, blendMask, autodisable, + speedmult, start, stop, startpoint, loops, loopfallback); + else + mAnimation->play( + groupname, priority, blendMask, autodisable, speedmult, start, stop, startpoint, loops, loopfallback); + } + + bool CharacterController::playGroup(std::string_view groupname, int mode, uint32_t count, bool scripted) { if (!mAnimation || !mAnimation->hasAnimation(groupname)) return false; - // We should not interrupt persistent animations by non-persistent ones - if (isPersistentAnimPlaying() && !persist) + // We should not interrupt scripted animations with non-scripted ones + if (isScriptedAnimPlaying() && !scripted) return true; - // If this animation is a looped animation (has a "loop start" key) that is already playing + bool looping = mAnimation->isLoopingAnimation(groupname); + + // If this animation is a looped animation that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count // and remove any other animations that were queued. // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners // correctly. - if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname - && mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 + if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && looping && mAnimation->isPlaying(groupname)) { float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop stop"); @@ -2516,51 +2616,92 @@ namespace MWMechanics } } - count = std::max(count, 1); + // The loop count in vanilla is weird. + // if played with a count of 0, all objects play exactly once from start to stop. + // But if the count is x > 0, actors and non-actors behave differently. actors will loop + // exactly x times, while non-actors will loop x+1 instead. + if (mPtr.getClass().isActor() && count > 0) + count--; AnimationQueueEntry entry; entry.mGroup = groupname; - entry.mLoopCount = count - 1; - entry.mPersist = persist; + entry.mLoopCount = count; + entry.mTime = 0.f; + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + entry.mScripted = (scripted && groupname != "idle"); + entry.mLooping = looping; + entry.mSpeed = 1.f; + entry.mStartKey = ((mode == 2) ? "loop start" : "start"); + entry.mStopKey = "stop"; + + bool playImmediately = false; if (mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { - clearAnimQueue(persist); + clearAnimQueue(scripted); - clearStateAnimation(mCurrentIdle); - - mIdleState = CharState_SpecialIdle; - bool loopfallback = entry.mGroup.starts_with("idle"); - mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default, - MWRender::Animation::BlendMask_All, false, 1.0f, ((mode == 2) ? "loop start" : "start"), "stop", 0.0f, - count - 1, loopfallback); + playImmediately = true; } else { mAnimQueue.resize(1); } - // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing - if (groupname == "idle") - entry.mPersist = false; - mAnimQueue.push_back(entry); + if (playImmediately) + playAnimQueue(mode == 2); + return true; } + bool CharacterController::playGroupLua(std::string_view groupname, float speed, std::string_view startKey, + std::string_view stopKey, uint32_t loops, bool forceLoop) + { + // Note: In mwscript, "idle" is a special case used to clear the anim queue. + // In lua we offer an explicit clear method instead so this method does not treat "idle" special. + + if (!mAnimation || !mAnimation->hasAnimation(groupname)) + return false; + + AnimationQueueEntry entry; + entry.mGroup = groupname; + // Note: MWScript gives one less loop to actors than non-actors. + // But this is the Lua version. We don't need to reproduce this weirdness here. + entry.mLoopCount = loops; + entry.mStartKey = startKey; + entry.mStopKey = stopKey; + entry.mLooping = mAnimation->isLoopingAnimation(groupname) || forceLoop; + entry.mScripted = true; + entry.mSpeed = speed; + entry.mTime = 0; + + if (mAnimQueue.size() > 1) + mAnimQueue.resize(1); + mAnimQueue.push_back(entry); + + if (mAnimQueue.size() == 1) + playAnimQueue(); + + return true; + } + + void CharacterController::enableLuaAnimations(bool enable) + { + mLuaAnimations = enable; + } + void CharacterController::skipAnim() { mSkipAnim = true; } - bool CharacterController::isPersistentAnimPlaying() const + bool CharacterController::isScriptedAnimPlaying() const { + // If the front of the anim queue is scripted, morrowind treats it as if it's + // still playing even if it's actually done. if (!mAnimQueue.empty()) - { - const AnimationQueueEntry& first = mAnimQueue.front(); - return first.mPersist && isAnimPlaying(first.mGroup); - } + return mAnimQueue.front().mScripted; return false; } @@ -2572,21 +2713,39 @@ namespace MWMechanics return mAnimation->isPlaying(groupName); } - void CharacterController::clearAnimQueue(bool clearPersistAnims) + bool CharacterController::isMovementAnimationControlled() const + { + if (mHitState != CharState_None) + return true; + + if (Settings::game().mPlayerMovementIgnoresAnimation && mPtr == getPlayer()) + return false; + + if (mInJump) + return false; + + bool movementAnimationControlled = mIdleState != CharState_None; + if (mMovementState != CharState_None) + movementAnimationControlled = mMovementAnimationHasMovement; + return movementAnimationControlled; + } + + void CharacterController::clearAnimQueue(bool clearScriptedAnims) { // Do not interrupt scripted animations, if we want to keep them - if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) + if ((!isScriptedAnimPlaying() || clearScriptedAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); - if (clearPersistAnims) + if (clearScriptedAnims) { + mAnimation->setPlayScriptedOnly(false); mAnimQueue.clear(); return; } for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { - if (!it->mPersist) + if (!it->mScripted) it = mAnimQueue.erase(it); else ++it; @@ -2602,7 +2761,7 @@ namespace MWMechanics // Make sure we canceled the current attack or spellcasting, // because we disabled attack animations anyway. mCanCast = false; - mCastingManualSpell = false; + mCastingScriptedSpell = false; setAttackingOrSpell(false); if (mUpperBodyState != UpperBodyState::None) mUpperBodyState = UpperBodyState::WeaponEquipped; @@ -2614,6 +2773,8 @@ namespace MWMechanics playRandomDeath(); } + updateAnimQueue(); + mAnimation->runAnimation(0.f); } @@ -2652,18 +2813,20 @@ namespace MWMechanics // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. // Stop any effects that are no longer active - std::vector effects; - mAnimation->getLoopingEffects(effects); + std::vector effects = mAnimation->getLoopingEffects(); - for (int effectId : effects) + for (std::string_view effectId : effects) { - if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() - || mPtr.getClass() - .getCreatureStats(mPtr) - .getMagicEffects() - .getOrDefault(MWMechanics::EffectKey(effectId)) - .getMagnitude() - <= 0) + auto index = ESM::MagicEffect::indexNameToIndex(effectId); + + if (index >= 0 + && (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() + || mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(MWMechanics::EffectKey(index)) + .getMagnitude() + <= 0)) mAnimation->removeEffect(effectId); } } @@ -2750,7 +2913,7 @@ namespace MWMechanics bool CharacterController::isCastingSpell() const { - return mCastingManualSpell || mUpperBodyState == UpperBodyState::Casting; + return mCastingScriptedSpell || mUpperBodyState == UpperBodyState::Casting; } bool CharacterController::isReadyToBlock() const @@ -2804,10 +2967,10 @@ namespace MWMechanics mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell); } - void CharacterController::castSpell(const ESM::RefId& spellId, bool manualSpell) + void CharacterController::castSpell(const ESM::RefId& spellId, bool scriptedSpell) { setAttackingOrSpell(true); - mCastingManualSpell = manualSpell; + mCastingScriptedSpell = scriptedSpell; ActionSpell action = ActionSpell(spellId); action.prepare(mPtr); } @@ -2852,6 +3015,11 @@ namespace MWMechanics return mPtr.getClass().getCreatureStats(mPtr).getAttackingOrSpell(); } + std::string_view CharacterController::getDesiredAttackType() const + { + return mPtr.getClass().getCreatureStats(mPtr).getAttackType(); + } + void CharacterController::setActive(int active) const { mAnimation->setActive(active); @@ -2883,6 +3051,39 @@ namespace MWMechanics MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch); } + float CharacterController::getAnimationMovementDirection() const + { + switch (mMovementState) + { + case CharState_RunLeft: + case CharState_SneakLeft: + case CharState_SwimWalkLeft: + case CharState_SwimRunLeft: + case CharState_WalkLeft: + return osg::PI_2f; + case CharState_RunRight: + case CharState_SneakRight: + case CharState_SwimWalkRight: + case CharState_SwimRunRight: + case CharState_WalkRight: + return -osg::PI_2f; + case CharState_RunForward: + case CharState_SneakForward: + case CharState_SwimRunForward: + case CharState_SwimWalkForward: + case CharState_WalkForward: + return mAnimation->getLegsYawRadians(); + case CharState_RunBack: + case CharState_SneakBack: + case CharState_SwimWalkBack: + case CharState_SwimRunBack: + case CharState_WalkBack: + return mAnimation->getLegsYawRadians() - osg::PIf; + default: + return 0.0f; + } + } + void CharacterController::updateHeadTracking(float duration) { const osg::Node* head = mAnimation->getNode("Bip01 Head"); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 5a676e3f6d..f043419a81 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -40,7 +40,7 @@ namespace MWMechanics Priority_Torch, Priority_Storm, Priority_Death, - Priority_Persistent, + Priority_Scripted, Num_Priorities }; @@ -134,11 +134,17 @@ namespace MWMechanics struct AnimationQueueEntry { std::string mGroup; - size_t mLoopCount; - bool mPersist; + uint32_t mLoopCount; + float mTime; + bool mLooping; + bool mScripted; + std::string mStartKey; + std::string mStopKey; + float mSpeed; }; typedef std::deque AnimationQueue; AnimationQueue mAnimQueue; + bool mLuaAnimations{ false }; CharacterState mIdleState{ CharState_None }; std::string mCurrentIdle; @@ -147,7 +153,7 @@ namespace MWMechanics std::string mCurrentMovement; float mMovementAnimSpeed{ 0.f }; bool mAdjustMovementAnimSpeed{ false }; - bool mMovementAnimationControlled{ true }; + bool mMovementAnimationHasMovement{ false }; CharacterState mDeathState{ CharState_None }; std::string mCurrentDeath; @@ -186,7 +192,7 @@ namespace MWMechanics bool mCanCast{ false }; - bool mCastingManualSpell{ false }; + bool mCastingScriptedSpell{ false }; bool mIsMovingBackward{ false }; osg::Vec2f mSmoothedSpeed; @@ -207,17 +213,16 @@ namespace MWMechanics void refreshMovementAnims(CharacterState movement, bool force = false); void refreshIdleAnims(CharacterState idle, bool force = false); - void clearAnimQueue(bool clearPersistAnims = false); - bool updateWeaponState(); void updateIdleStormState(bool inwater) const; std::string chooseRandomAttackAnimation() const; static bool isRandomAttackAnimation(std::string_view group); - bool isPersistentAnimPlaying() const; + bool isMovementAnimationControlled() const; void updateAnimQueue(); + void playAnimQueue(bool useLoopStart = false); void updateHeadTracking(float duration); @@ -242,6 +247,8 @@ namespace MWMechanics bool getAttackingOrSpell() const; void setAttackingOrSpell(bool attackingOrSpell) const; + std::string_view getDesiredAttackType() const; + void prepareHit(); public: @@ -269,9 +276,17 @@ namespace MWMechanics void persistAnimationState() const; void unpersistAnimationState(); - bool playGroup(std::string_view groupname, int mode, int count, bool persist = false); + void playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, int blendMask, + bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, + uint32_t loops, bool loopfallback = false) const; + bool playGroup(std::string_view groupname, int mode, uint32_t count, bool scripted = false); + bool playGroupLua(std::string_view groupname, float speed, std::string_view startKey, std::string_view stopKey, + uint32_t loops, bool forceLoop); + void enableLuaAnimations(bool enable); void skipAnim(); bool isAnimPlaying(std::string_view groupName) const; + bool isScriptedAnimPlaying() const; + void clearAnimQueue(bool clearScriptedAnims = false); enum KillResult { @@ -299,7 +314,7 @@ namespace MWMechanics bool isAttackingOrSpell() const; void setVisibility(float visibility) const; - void castSpell(const ESM::RefId& spellId, bool manualSpell = false); + void castSpell(const ESM::RefId& spellId, bool scriptedSpell = false); void setAIAttackType(std::string_view attackType); static std::string_view getRandomAttackType(); @@ -318,6 +333,8 @@ namespace MWMechanics void playSwishSound() const; + float getAnimationMovementDirection() const; + MWWorld::MovementDirectionFlags getSupportedMovementDirections() const; }; } diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 1c9c0c1dee..5d283214a3 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -10,6 +10,7 @@ #include #include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -134,6 +135,15 @@ namespace MWMechanics auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (Misc::Rng::roll0to99(prng) < x) { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + const ESM::RefId skill = shield->getClass().getEquipmentSkill(*shield); + if (skill == ESM::Skill::LightArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::MediumArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::HeavyArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); + // Reduce shield durability by incoming damage int shieldhealth = shield->getClass().getItemHealth(*shield); @@ -157,7 +167,7 @@ namespace MWMechanics blockerStats.setBlock(true); if (blocker == getPlayer()) - blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); + blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, ESM::Skill::Block_Success); return true; } @@ -230,19 +240,22 @@ namespace MWMechanics if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); + victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, + MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } - const unsigned char* attack = weapon.get()->mBase->mData.mChop; - damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); // Bow/crossbow damage - - // Arrow/bolt damage - // NB in case of thrown weapons, we are applying the damage twice since projectile == weapon - attack = projectile.get()->mBase->mData.mChop; - damage += attack[0] + ((attack[1] - attack[0]) * attackStrength); - + { + const auto& attack = weapon.get()->mBase->mData.mChop; + damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); // Bow/crossbow damage + } + { + // Arrow/bolt damage + // NB in case of thrown weapons, we are applying the damage twice since projectile == weapon + const auto& attack = projectile.get()->mBase->mData.mChop; + damage += attack[0] + ((attack[1] - attack[0]) * attackStrength); + } adjustWeaponDamage(damage, weapon, attacker); } @@ -256,7 +269,7 @@ namespace MWMechanics applyWerewolfDamageMult(victim, projectile, damage); if (attacker == getPlayer()) - attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0); + attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& sequence = victim.getClass().getCreatureStats(victim).getAiSequence(); bool unaware = attacker == getPlayer() && !sequence.isInCombat() @@ -286,7 +299,8 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1); } - victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); + victim.getClass().onHit( + victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); } } @@ -551,4 +565,142 @@ namespace MWMechanics return distanceIgnoreZ(lhs, rhs); return distance(lhs, rhs); } + + float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + { + osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + MWBase::World* world = MWBase::Environment::get().getWorld(); + + float dist = (targetPos - actorPos).length(); + dist -= world->getHalfExtents(actor).y(); + dist -= world->getHalfExtents(target).y(); + return dist; + } + + std::pair getHitContact(const MWWorld::Ptr& actor, float reach) + { + // Lasciate ogne speranza, voi ch'entrate + MWWorld::Ptr result; + osg::Vec3f hitPos; + float minDist = std::numeric_limits::max(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + + // These GMSTs are not in degrees. They're tolerance angle sines multiplied by 90. + // With the default values of 60, the actual tolerance angles are roughly 41.8 degrees. + // Don't think too hard about it. In this place, thinking can cause permanent damage to your mental health. + const float fCombatAngleXY = store.find("fCombatAngleXY")->mValue.getFloat() / 90.f; + const float fCombatAngleZ = store.find("fCombatAngleZ")->mValue.getFloat() / 90.f; + + const ESM::Position& posdata = actor.getRefData().getPosition(); + const osg::Vec3f actorPos(posdata.asVec3()); + const osg::Vec3f actorDirXY = osg::Quat(posdata.rot[2], osg::Vec3(0, 0, -1)) * osg::Vec3f(0, 1, 0); + // Only the player can look up, apparently. + const float actorVerticalAngle = actor == getPlayer() ? -std::sin(posdata.rot[0]) : 0.f; + const float actorEyeLevel = world->getHalfExtents(actor, true).z() * 2.f * 0.85f; + const osg::Vec3f actorEyePos{ actorPos.x(), actorPos.y(), actorPos.z() + actorEyeLevel }; + const bool canMoveByZ = canActorMoveByZAxis(actor); + + // The player can target any active actor, non-playable actors only target their targets + std::vector targets; + if (actor != getPlayer()) + actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targets); + else + MWBase::Environment::get().getMechanicsManager()->getActorsInRange( + actorPos, Settings::game().mActorsProcessingRange, targets); + + for (MWWorld::Ptr& target : targets) + { + if (actor == target || target.getClass().getCreatureStats(target).isDead()) + continue; + const float dist = getDistanceToBounds(actor, target); + const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + if (dist >= reach || dist >= minDist || std::abs(targetPos.z() - actorPos.z()) >= reach) + continue; + + // Horizontal angle checks. + osg::Vec2f actorToTargetXY{ targetPos.x() - actorPos.x(), targetPos.y() - actorPos.y() }; + actorToTargetXY.normalize(); + + // Use dot product to check if the target is behind first... + if (actorToTargetXY.x() * actorDirXY.x() + actorToTargetXY.y() * actorDirXY.y() <= 0.f) + continue; + + // And then perp dot product to calculate the hit angle sine. + // This gives us a horizontal hit range of [-asin(fCombatAngleXY / 90); asin(fCombatAngleXY / 90)] + if (std::abs(actorToTargetXY.x() * actorDirXY.y() - actorToTargetXY.y() * actorDirXY.x()) > fCombatAngleXY) + continue; + + // Vertical angle checks. Nice cliff racer hack, Todd. + if (!canMoveByZ) + { + // The idea is that the body should always be possible to hit. + // fCombatAngleZ is the tolerance for hitting the target's feet or head. + osg::Vec3f actorToTargetFeet = targetPos - actorEyePos; + osg::Vec3f actorToTargetHead = actorToTargetFeet; + actorToTargetFeet.normalize(); + actorToTargetHead.z() += world->getHalfExtents(target, true).z() * 2.f; + actorToTargetHead.normalize(); + + if (actorVerticalAngle - actorToTargetHead.z() > fCombatAngleZ + || actorVerticalAngle - actorToTargetFeet.z() < -fCombatAngleZ) + continue; + } + + // Gotta use physics somehow! + if (!world->getLOS(actor, target)) + continue; + + minDist = dist; + result = target; + } + + // This hit position is currently used for spawning the blood effect. + // Morrowind does this elsewhere, but roughly at the same time + // and it would be hard to track the original hit results outside of this function + // without code duplication + // The idea is to use a random point on a plane in front of the target + // that is defined by its width and height + if (!result.isEmpty()) + { + osg::Vec3f resultPos(result.getRefData().getPosition().asVec3()); + osg::Vec3f dirToActor = actorPos - resultPos; + dirToActor.normalize(); + + hitPos = resultPos + dirToActor * world->getHalfExtents(result).y(); + // -25% to 25% of width + float xOffset = Misc::Rng::deviate(0.f, 0.25f, world->getPrng()); + // 20% to 100% of height + float zOffset = Misc::Rng::deviate(0.6f, 0.4f, world->getPrng()); + hitPos.x() += world->getHalfExtents(result).x() * 2.f * xOffset; + hitPos.z() += world->getHalfExtents(result).z() * 2.f * zOffset; + } + + return std::make_pair(result, hitPos); + } + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain) + { + const MWWorld::Ptr& player = getPlayer(); + if (attacker != player) + return false; + + std::set followersAttacker; + MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(attacker, followersAttacker); + if (followersAttacker.find(target) == followersAttacker.end()) + return false; + + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); + if (statsTarget.getAiSequence().isInCombat()) + return true; + statsTarget.friendlyHit(); + if (statsTarget.getFriendlyHits() >= 4) + return false; + + if (complain) + MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); + return true; + } + } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 2e7caf6189..92033c7e77 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MECHANICS_COMBAT_H #define OPENMW_MECHANICS_COMBAT_H +#include + namespace osg { class Vec3f; @@ -59,6 +61,13 @@ namespace MWMechanics float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); + // Cursed distance calculation used for combat proximity and hit checks in Morrowind + float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + + // Similarly cursed hit target selection + std::pair getHitContact(const MWWorld::Ptr& actor, float reach); + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain); } #endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 757bdf7c49..b18bca98d5 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -20,31 +20,6 @@ namespace MWMechanics int CreatureStats::sActorId = 0; CreatureStats::CreatureStats() - : mDrawState(DrawState::Nothing) - , mDead(false) - , mDeathAnimationFinished(false) - , mDied(false) - , mMurdered(false) - , mFriendlyHits(0) - , mTalkedTo(false) - , mAlarmed(false) - , mAttacked(false) - , mKnockdown(false) - , mKnockdownOneFrame(false) - , mKnockdownOverOneFrame(false) - , mHitRecovery(false) - , mBlock(false) - , mMovementFlags(0) - , mFallHeight(0) - , mLastRestock(0, 0) - , mGoldPool(0) - , mActorId(-1) - , mHitAttemptActorId(-1) - , mDeathAnimation(-1) - , mTimeOfDeath() - , mSideMovementAngle(0) - , mLevel(0) - , mAttackingOrSpell(false) { for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) { @@ -241,6 +216,11 @@ namespace MWMechanics bool CreatureStats::isParalyzed() const { + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Ptr player = world->getPlayerPtr(); + if (world->getGodModeState() && this == &player.getClass().getCreatureStats(player)) + return false; + return mMagicEffects.getOrDefault(ESM::MagicEffect::Paralyze).getMagnitude() > 0; } @@ -319,6 +299,11 @@ namespace MWMechanics ++mFriendlyHits; } + void CreatureStats::resetFriendlyHits() + { + mFriendlyHits = 0; + } + bool CreatureStats::hasTalkedToPlayer() const { return mTalkedTo; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index f0a834bd33..f65e1a000f 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -39,30 +39,30 @@ namespace MWMechanics class CreatureStats { static int sActorId; - DrawState mDrawState; std::map mAttributes; DynamicStat mDynamic[3]; // health, magicka, fatigue + DrawState mDrawState = DrawState::Nothing; Spells mSpells; ActiveSpells mActiveSpells; MagicEffects mMagicEffects; Stat mAiSettings[4]; AiSequence mAiSequence; - bool mDead; - bool mDeathAnimationFinished; - bool mDied; // flag for OnDeath script function - bool mMurdered; - int mFriendlyHits; - bool mTalkedTo; - bool mAlarmed; - bool mAttacked; - bool mKnockdown; - bool mKnockdownOneFrame; - bool mKnockdownOverOneFrame; - bool mHitRecovery; - bool mBlock; - unsigned int mMovementFlags; + bool mDead = false; + bool mDeathAnimationFinished = false; + bool mDied = false; // flag for OnDeath script function + bool mMurdered = false; + int mFriendlyHits = 0; + bool mTalkedTo = false; + bool mAlarmed = false; + bool mAttacked = false; + bool mKnockdown = false; + bool mKnockdownOneFrame = false; + bool mKnockdownOverOneFrame = false; + bool mHitRecovery = false; + bool mBlock = false; + unsigned int mMovementFlags = 0; - float mFallHeight; + float mFallHeight = 0.f; ESM::RefId mLastHitObject; // The last object to hit this actor ESM::RefId mLastHitAttemptObject; // The last object to attempt to hit this actor @@ -71,21 +71,17 @@ namespace MWMechanics MWWorld::TimeStamp mLastRestock; // The pool of merchant gold (not in inventory) - int mGoldPool; + int mGoldPool = 0; - int mActorId; - int mHitAttemptActorId; // Stores an actor that attacked this actor. Only one is stored at a time, - // and it is not changed if a different actor attacks. It is cleared when combat ends. - - // The index of the death animation that was played, or -1 if none played - signed char mDeathAnimation; - - MWWorld::TimeStamp mTimeOfDeath; + int mActorId = -1; + // Stores an actor that attacked this actor. Only one is stored at a time, and it is not changed if a different + // actor attacks. It is cleared when combat ends. + int mHitAttemptActorId = -1; // The difference between view direction and lower body direction. - float mSideMovementAngle; + float mSideMovementAngle = 0; - bool mTeleported = false; + MWWorld::TimeStamp mTimeOfDeath; private: std::multimap mSummonedCreatures; // @@ -95,8 +91,15 @@ namespace MWMechanics std::vector mSummonGraveyard; protected: - int mLevel; - bool mAttackingOrSpell; + std::string mAttackType; + int mLevel = 0; + bool mAttackingOrSpell = false; + + private: + // The index of the death animation that was played, or -1 if none played + signed char mDeathAnimation = -1; + + bool mTeleported = false; public: CreatureStats(); @@ -130,6 +133,7 @@ namespace MWMechanics const MagicEffects& getMagicEffects() const; bool getAttackingOrSpell() const { return mAttackingOrSpell; } + std::string_view getAttackType() const { return mAttackType; } int getLevel() const; @@ -156,6 +160,8 @@ namespace MWMechanics void setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; } + void setAttackType(std::string_view attackType) { mAttackType = attackType; } + void setLevel(int level); void setAiSetting(AiSetting index, Stat value); @@ -200,6 +206,8 @@ namespace MWMechanics void friendlyHit(); ///< Increase number of friendly hits by one. + void resetFriendlyHits(); + bool hasTalkedToPlayer() const; ///< Has this creature talked with the player before? @@ -294,7 +302,7 @@ namespace MWMechanics bool wasTeleported() const { return mTeleported; } void setTeleported(bool v) { mTeleported = v; } - const std::map getAttributes() const { return mAttributes; } + const std::map& getAttributes() const { return mAttributes; } }; } diff --git a/apps/openmw/mwmechanics/damagesourcetype.hpp b/apps/openmw/mwmechanics/damagesourcetype.hpp new file mode 100644 index 0000000000..e140a8106f --- /dev/null +++ b/apps/openmw/mwmechanics/damagesourcetype.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H +#define OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H + +namespace MWMechanics +{ + enum class DamageSourceType + { + Unspecified, + Melee, + Ranged, + Magical, + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 13894146b3..7d0007f9e3 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -84,7 +84,8 @@ namespace MWMechanics if (getEnchantChance() <= (Misc::Rng::roll0to99(prng))) return false; - mEnchanter.getClass().skillUsageSucceeded(mEnchanter, ESM::Skill::Enchant, 2); + mEnchanter.getClass().skillUsageSucceeded( + mEnchanter, ESM::Skill::Enchant, ESM::Skill::Enchant_CreateMagicItem); } enchantment.mEffects = mEffectList; @@ -197,13 +198,13 @@ namespace MWMechanics float enchantmentCost = 0.f; float cost = 0.f; - for (const ESM::ENAMstruct& effect : mEffectList.mList) + for (const ESM::IndexedENAMstruct& effect : mEffectList.mList) { - float baseCost = (store.get().find(effect.mEffectID))->mData.mBaseCost; - int magMin = std::max(1, effect.mMagnMin); - int magMax = std::max(1, effect.mMagnMax); - int area = std::max(1, effect.mArea); - float duration = static_cast(effect.mDuration); + float baseCost = (store.get().find(effect.mData.mEffectID))->mData.mBaseCost; + int magMin = std::max(1, effect.mData.mMagnMin); + int magMax = std::max(1, effect.mData.mMagnMax); + int area = std::max(1, effect.mData.mArea); + float duration = static_cast(effect.mData.mDuration); if (mCastStyle == ESM::Enchantment::ConstantEffect) duration = fEnchantmentConstantDurationMult; @@ -211,7 +212,7 @@ namespace MWMechanics cost = std::max(1.f, cost); - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) cost *= 1.5f; enchantmentCost += precise ? cost : std::floor(cost); @@ -243,13 +244,7 @@ namespace MWMechanics for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) { - const ESM::ENAMstruct& first = iter->mEffects.mList[i]; - const ESM::ENAMstruct& second = toFind.mEffects.mList[i]; - - if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange - || first.mSkill != second.mSkill || first.mAttribute != second.mAttribute - || first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax - || first.mDuration != second.mDuration) + if (iter->mEffects.mList[i] != toFind.mEffects.mList[i]) { mismatch = true; break; diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 0d626c9e11..c2afef7c0d 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -48,7 +48,7 @@ namespace MWMechanics std::string EffectKey::toString() const { const auto& store = MWBase::Environment::get().getESMStore(); - const ESM::MagicEffect* magicEffect = store->get().search(mId); + const ESM::MagicEffect* magicEffect = store->get().find(mId); return getMagicEffectString( *magicEffect, store->get().search(mArg), store->get().search(mArg)); } @@ -64,6 +64,11 @@ namespace MWMechanics return left.mArg < right.mArg; } + bool operator==(const EffectKey& left, const EffectKey& right) + { + return left.mId == right.mId && left.mArg == right.mArg; + } + float EffectParam::getMagnitude() const { return mBase + mModifier; diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index b9831c0250..4fe5d9fd4e 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -38,6 +38,7 @@ namespace MWMechanics }; bool operator<(const EffectKey& left, const EffectKey& right); + bool operator==(const EffectKey& left, const EffectKey& right); struct EffectParam { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index ff720345bc..46f6440ae6 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1,5 +1,7 @@ #include "mechanicsmanagerimp.hpp" +#include + #include #include @@ -22,11 +24,15 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwsound/constants.hpp" + #include "actor.hpp" +#include "actors.hpp" #include "actorutil.hpp" #include "aicombat.hpp" #include "aipursue.hpp" @@ -131,14 +137,9 @@ namespace MWMechanics for (size_t i = 0; i < player->mNpdt.mSkills.size(); ++i) npcStats.getSkill(ESM::Skill::indexToRefId(i)).setBase(player->mNpdt.mSkills[i]); - creatureStats.setAttribute(ESM::Attribute::Strength, player->mNpdt.mStrength); - creatureStats.setAttribute(ESM::Attribute::Intelligence, player->mNpdt.mIntelligence); - creatureStats.setAttribute(ESM::Attribute::Willpower, player->mNpdt.mWillpower); - creatureStats.setAttribute(ESM::Attribute::Agility, player->mNpdt.mAgility); - creatureStats.setAttribute(ESM::Attribute::Speed, player->mNpdt.mSpeed); - creatureStats.setAttribute(ESM::Attribute::Endurance, player->mNpdt.mEndurance); - creatureStats.setAttribute(ESM::Attribute::Personality, player->mNpdt.mPersonality); - creatureStats.setAttribute(ESM::Attribute::Luck, player->mNpdt.mLuck); + for (size_t i = 0; i < player->mNpdt.mAttributes.size(); ++i) + npcStats.setAttribute(ESM::Attribute::indexToRefId(i), player->mNpdt.mSkills[i]); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); // race @@ -149,12 +150,7 @@ namespace MWMechanics bool male = (player->mFlags & ESM::NPC::Female) == 0; for (const ESM::Attribute& attribute : esmStore.get()) - { - const ESM::Race::MaleFemale& value - = race->mData.mAttributeValues[static_cast(ESM::Attribute::refIdToIndex(attribute.mId))]; - - creatureStats.setAttribute(attribute.mId, male ? value.mMale : value.mFemale); - } + creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); for (const ESM::Skill& skill : esmStore.get()) { @@ -266,10 +262,10 @@ namespace MWMechanics mObjects.addObject(ptr); } - void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) + void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) { if (ptr.getClass().isActor()) - mActors.castSpell(ptr, spellId, manualSpell); + mActors.castSpell(ptr, spellId, scriptedSpell); } void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) @@ -477,8 +473,8 @@ namespace MWMechanics int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { - const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); - float x = static_cast(npcSkill.getBaseDisposition()); + const MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + float x = static_cast(npcStats.getBaseDisposition() + npcStats.getCrimeDispositionModifier()); MWWorld::LiveCellRef* npc = ptr.get(); MWWorld::Ptr playerPtr = getPlayer(); @@ -753,12 +749,27 @@ namespace MWMechanics } bool MechanicsManager::playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) { if (ptr.getClass().isActor()) - return mActors.playAnimationGroup(ptr, groupName, mode, number, persist); + return mActors.playAnimationGroup(ptr, groupName, mode, number, scripted); else - return mObjects.playAnimationGroup(ptr, groupName, mode, number, persist); + return mObjects.playAnimationGroup(ptr, groupName, mode, number, scripted); + } + bool MechanicsManager::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + if (ptr.getClass().isActor()) + return mActors.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + else + return mObjects.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + } + void MechanicsManager::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + if (ptr.getClass().isActor()) + mActors.enableLuaAnimations(ptr, enable); + else + mObjects.enableLuaAnimations(ptr, enable); } void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr) { @@ -775,6 +786,14 @@ namespace MWMechanics return false; } + bool MechanicsManager::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const + { + if (ptr.getClass().isActor()) + return mActors.checkScriptedAnimationPlaying(ptr); + + return false; + } + bool MechanicsManager::onOpen(const MWWorld::Ptr& ptr) { if (ptr.getClass().isActor()) @@ -795,6 +814,14 @@ namespace MWMechanics mObjects.persistAnimationStates(); } + void MechanicsManager::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + if (ptr.getClass().isActor()) + mActors.clearAnimationQueue(ptr, clearScripted); + else + mObjects.clearAnimationQueue(ptr, clearScripted); + } + void MechanicsManager::updateMagicEffects(const MWWorld::Ptr& ptr) { mActors.updateMagicEffects(ptr); @@ -819,14 +846,12 @@ namespace MWMechanics mAI = true; } - bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) + namespace { - static std::set boundItemIDCache; - - // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's - // for some reason - if (boundItemIDCache.empty()) + std::set makeBoundItemIdCache() { + std::set boundItemIDCache; + // Build a list of known bound item ID's const MWWorld::Store& gameSettings = MWBase::Environment::get().getESMStore()->get(); @@ -843,15 +868,16 @@ namespace MWMechanics boundItemIDCache.insert(ESM::RefId::stringRefId(currentGMSTValue)); } + + return boundItemIDCache; } + } - // Perform bound item check and assign the Flag_Bound bit if it passes - const ESM::RefId& tempItemID = item.getCellRef().getRefId(); + bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) + { + static const std::set boundItemIdCache = makeBoundItemIdCache(); - if (boundItemIDCache.count(tempItemID) != 0) - return true; - - return false; + return boundItemIdCache.find(item.getCellRef().getRefId()) != boundItemIdCache.end(); } bool MechanicsManager::isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) @@ -1026,7 +1052,7 @@ namespace MWMechanics if (stolenIt == mStolenItems.end()) continue; OwnerMap& owners = stolenIt->second; - int itemCount = it->getRefData().getCount(); + int itemCount = it->getCellRef().getCount(); for (OwnerMap::iterator ownerIt = owners.begin(); ownerIt != owners.end();) { int toRemove = std::min(itemCount, ownerIt->second); @@ -1038,7 +1064,7 @@ namespace MWMechanics ++ownerIt; } - int toMove = it->getRefData().getCount() - itemCount; + int toMove = it->getCellRef().getCount() - itemCount; containerStore.add(*it, toMove); store.remove(*it, toMove); @@ -1088,15 +1114,21 @@ namespace MWMechanics } } - if (!(item.getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId)) + const bool isGold = item.getClass().isGold(item); + if (!isGold) { if (victim.isEmpty() - || (victim.getClass().isActor() && victim.getRefData().getCount() > 0 + || (victim.getClass().isActor() && victim.getCellRef().getCount() > 0 && !victim.getClass().getCreatureStats(victim).isDead())) mStolenItems[item.getCellRef().getRefId()][owner] += count; } if (alarm) - commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), item.getClass().getValue(item) * count); + { + int value = count; + if (!isGold) + value *= item.getClass().getValue(item); + commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), value); + } } bool MechanicsManager::commitCrime(const MWWorld::Ptr& player, const MWWorld::Ptr& victim, OffenseType type, @@ -1159,9 +1191,9 @@ namespace MWMechanics && !victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) reported = reportCrime(player, victim, type, ESM::RefId(), arg); + // TODO: combat should be started with an "unaware" flag, which makes the victim flee? if (!reported) - startCombat(victim, - player); // TODO: combat should be started with an "unaware" flag, which makes the victim flee? + startCombat(victim, player, &playerFollowers); } return crimeSeen; } @@ -1291,67 +1323,140 @@ namespace MWMechanics if (!canReportCrime(actor, victim, playerFollowers)) continue; - if (reported && actor.getClass().isClass(actor, "guard")) + NpcStats& observerStats = actor.getClass().getNpcStats(actor); + + int alarm = observerStats.getAiSetting(AiSetting::Alarm).getBase(); + float alarmTerm = 0.01f * alarm; + + bool isActorVictim = actor == victim; + float dispTerm = isActorVictim ? dispVictim : disp; + + bool isActorGuard = actor.getClass().isClass(actor, "guard"); + + int currentDisposition = getDerivedDisposition(actor); + + bool isPermanent = false; + bool applyOnlyIfHostile = false; + int dispositionModifier = 0; + // Murdering and trespassing seem to do not affect disposition + if (type == OT_Theft) + { + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (type == OT_Pickpocket) + { + if (alarm >= 100 && isActorGuard) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (isActorVictim) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + } + else if (type == OT_Assault) + { + if (isActorVictim && !isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + else if (alarm >= 100) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else + { + applyOnlyIfHostile = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + } + + bool setCrimeId = false; + if (isPermanent && dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + int baseDisposition = observerStats.getBaseDisposition(); + observerStats.setBaseDisposition(baseDisposition + dispositionModifier); + } + else if (dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + + if (isActorGuard && alarm >= 100) { // Mark as Alarmed for dialogue - actor.getClass().getCreatureStats(actor).setAlarmed(true); + observerStats.setAlarmed(true); - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - actor.getClass().getNpcStats(actor).setCrimeId(id); + setCrimeId = true; - if (!actor.getClass().getCreatureStats(actor).getAiSequence().isInPursuit()) + if (!observerStats.getAiSequence().isInPursuit()) { - actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); + observerStats.getAiSequence().stack(AiPursue(player), actor); } } else { - float dispTerm = (actor == victim) ? dispVictim : disp; - - float alarmTerm - = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase(); - if (type == OT_Pickpocket && alarmTerm <= 0) + // If Alarm is 0, treat it like 100 to calculate a Fight modifier for a victim of pickpocketing. + // Observers which do not try to arrest player do not care about pickpocketing at all. + if (type == OT_Pickpocket && isActorVictim && alarmTerm == 0.0) alarmTerm = 1.0; + else if (type == OT_Pickpocket && !isActorVictim) + alarmTerm = 0.0; - if (actor != victim) - dispTerm *= alarmTerm; - - float fightTerm = static_cast((actor == victim) ? fightVictim : fight); + float fightTerm = static_cast(isActorVictim ? fightVictim : fight); fightTerm += getFightDispositionBias(dispTerm); fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; - const int observerFightRating - = actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Fight).getBase(); + const int observerFightRating = observerStats.getAiSetting(AiSetting::Fight).getBase(); if (observerFightRating + fightTerm > 100) fightTerm = static_cast(100 - observerFightRating); fightTerm = std::max(0.f, fightTerm); if (observerFightRating + fightTerm >= 100) { - startCombat(actor, player); + if (dispositionModifier != 0 && applyOnlyIfHostile) + { + dispositionModifier + = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + + startCombat(actor, player, &playerFollowers); - NpcStats& observerStats = actor.getClass().getNpcStats(actor); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off observerStats.setAiSetting(AiSetting::Fight, observerFightRating + static_cast(fightTerm)); - observerStats.setBaseDisposition(observerStats.getBaseDisposition() + static_cast(dispTerm)); - - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - observerStats.setCrimeId(id); + setCrimeId = true; // Mark as Alarmed for dialogue observerStats.setAlarmed(true); } } + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid and restore their disposition to player character. + if (setCrimeId) + observerStats.setCrimeId(id); } if (reported) { - player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() + arg); + player.getClass().getNpcStats(player).setBounty( + std::max(0, player.getClass().getNpcStats(player).getBounty() + arg)); // If committing a crime against a faction member, expell from the faction if (!victim.isEmpty() && victim.getClass().isNpc()) @@ -1361,7 +1466,7 @@ namespace MWMechanics const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); if (playerRanks.find(factionID) != playerRanks.end()) { - player.getClass().getNpcStats(player).expell(factionID); + player.getClass().getNpcStats(player).expell(factionID, true); } } else if (!factionId.empty()) @@ -1369,7 +1474,7 @@ namespace MWMechanics const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); if (playerRanks.find(factionId) != playerRanks.end()) { - player.getClass().getNpcStats(player).expell(factionId); + player.getClass().getNpcStats(player).expell(factionId, true); } } @@ -1380,7 +1485,7 @@ namespace MWMechanics // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. if (!victim.getClass().getCreatureStats(victim).getAiSequence().isInPursuit()) - startCombat(victim, player); + startCombat(victim, player, &playerFollowers); // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. @@ -1397,26 +1502,10 @@ namespace MWMechanics if (target == player || !attacker.getClass().isActor()) return false; - MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); - if (attacker == player) - { - std::set followersAttacker; - getActorsSidingWith(attacker, followersAttacker); - if (followersAttacker.find(target) != followersAttacker.end()) - { - statsTarget.friendlyHit(); - - if (statsTarget.getFriendlyHits() < 4) - { - MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); - return false; - } - } - } - if (canCommitCrimeAgainst(target, attacker)) commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); AiSequence& seq = statsTarget.getAiSequence(); if (!attacker.isEmpty() @@ -1441,14 +1530,14 @@ namespace MWMechanics if (!peaceful) { - startCombat(target, attacker); + SidingCache cachedAllies{ mActors, false }; + const std::set& attackerAllies = cachedAllies.getActorsSidingWith(attacker); + startCombat(target, attacker, &attackerAllies); // Force friendly actors into combat to prevent infighting between followers - std::set followersTarget; - getActorsSidingWith(target, followersTarget); - for (const auto& follower : followersTarget) + for (const auto& follower : cachedAllies.getActorsSidingWith(target)) { if (follower != attacker && follower != player) - startCombat(follower, attacker); + startCombat(follower, attacker, &attackerAllies); } } } @@ -1459,10 +1548,13 @@ namespace MWMechanics bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr& target, const MWWorld::Ptr& attacker) { - const MWMechanics::AiSequence& seq = target.getClass().getCreatureStats(target).getAiSequence(); - return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) - && !isAggressive(target, attacker) && !seq.isEngagedWithActor() - && !target.getClass().getCreatureStats(target).getAiSequence().isInPursuit(); + const MWWorld::Class& cls = target.getClass(); + const MWMechanics::CreatureStats& stats = cls.getCreatureStats(target); + const MWMechanics::AiSequence& seq = stats.getAiSequence(); + return cls.isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) && !isAggressive(target, attacker) + && !seq.isEngagedWithActor() && !stats.getAiSequence().isInPursuit() + && !cls.getNpcStats(target).isWerewolf() + && stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Vampirism).getMagnitude() <= 0; } void MechanicsManager::actorKilled(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) @@ -1569,7 +1661,8 @@ namespace MWMechanics return (Misc::Rng::roll0to99(prng) >= target); } - void MechanicsManager::startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) + void MechanicsManager::startCombat( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); @@ -1583,9 +1676,36 @@ namespace MWMechanics // We don't care about dialogue filters since the target is invalid. // We still want to play the combat taunt. MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack")); + if (!stats.getAiSequence().isInCombat()) + stats.resetFriendlyHits(); return; } + const bool inCombat = stats.getAiSequence().isInCombat(); + bool shout = !inCombat; + if (inCombat) + { + const auto isInCombatWithOneOf = [&](const auto& allies) { + for (const MWWorld::Ptr& ally : allies) + { + if (stats.getAiSequence().isInCombat(ally)) + return true; + } + return false; + }; + if (targetAllies) + shout = !isInCombatWithOneOf(*targetAllies); + else + { + shout = stats.getAiSequence().isInCombat(target); + if (!shout) + { + std::set sidingActors; + getActorsSidingWith(target, sidingActors); + shout = !isInCombatWithOneOf(sidingActors); + } + } + } stats.getAiSequence().stack(MWMechanics::AiCombat(target), ptr); if (target == getPlayer()) { @@ -1620,7 +1740,8 @@ namespace MWMechanics } // Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly - MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack")); + if (shout) + MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack")); } void MechanicsManager::stopCombat(const MWWorld::Ptr& actor) @@ -1830,11 +1951,7 @@ namespace MWMechanics // Transforming removes all temporary effects actor.getClass().getCreatureStats(actor).getActiveSpells().purge( - [](const auto& params) { - return params.getType() == ESM::ActiveSpells::Type_Consumable - || params.getType() == ESM::ActiveSpells::Type_Temporary; - }, - actor); + [](const auto& params) { return params.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, actor); mActors.updateActor(actor, 0.f); if (werewolf) @@ -1888,7 +2005,8 @@ namespace MWMechanics if (reported) { - npcStats.setBounty(npcStats.getBounty() + gmst.find("iWereWolfBounty")->mValue.getInteger()); + npcStats.setBounty( + std::max(0, npcStats.getBounty() + gmst.find("iWereWolfBounty")->mValue.getInteger())); } } } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 748965a682..4b0126cd34 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -11,6 +11,11 @@ #include "npcstats.hpp" #include "objects.hpp" +namespace MWSound +{ + enum class MusicType; +} + namespace MWWorld { class CellStore; @@ -100,7 +105,8 @@ namespace MWMechanics bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) override; /// Makes \a ptr fight \a target. Also shouts a combat taunt. - void startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; + void startCombat( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) override; void stopCombat(const MWWorld::Ptr& ptr) override; @@ -134,11 +140,16 @@ namespace MWMechanics /// Attempt to play an animation group /// @return Success or error - bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist = false) override; + bool playAnimationGroup(const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, + bool scripted = false) override; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) override; + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) override; void skipAnimation(const MWWorld::Ptr& ptr) override; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override; + bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override; void persistAnimationStates() override; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) override; /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) @@ -189,7 +200,7 @@ namespace MWMechanics /// Is \a ptr casting spell or using weapon now? bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const override; - void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell = false) override; + void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 3427e460c3..eceaf8b482 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -20,6 +20,7 @@ MWMechanics::NpcStats::NpcStats() : mDisposition(0) + , mCrimeDispositionModifier(0) , mReputation(0) , mCrimeId(-1) , mBounty(0) @@ -43,6 +44,21 @@ void MWMechanics::NpcStats::setBaseDisposition(int disposition) mDisposition = disposition; } +int MWMechanics::NpcStats::getCrimeDispositionModifier() const +{ + return mCrimeDispositionModifier; +} + +void MWMechanics::NpcStats::setCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier = value; +} + +void MWMechanics::NpcStats::modCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier += value; +} + const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill(ESM::RefId id) const { auto it = mSkills.find(id); @@ -112,14 +128,17 @@ bool MWMechanics::NpcStats::getExpelled(const ESM::RefId& factionID) const return mExpelled.find(factionID) != mExpelled.end(); } -void MWMechanics::NpcStats::expell(const ESM::RefId& factionID) +void MWMechanics::NpcStats::expell(const ESM::RefId& factionID, bool printMessage) { if (mExpelled.find(factionID) == mExpelled.end()) { + mExpelled.insert(factionID); + if (!printMessage) + return; + std::string message = "#{sExpelledMessage}"; message += MWBase::Environment::get().getESMStore()->get().find(factionID)->mName; MWBase::Environment::get().getWindowManager()->messageBox(message); - mExpelled.insert(factionID); } } @@ -190,95 +209,16 @@ float MWMechanics::NpcStats::getSkillProgressRequirement(ESM::RefId id, const ES return progressRequirement; } -void MWMechanics::NpcStats::useSkill(ESM::RefId id, const ESM::Class& class_, int usageType, float extraFactor) -{ - const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().find(id); - float skillGain = 1; - if (usageType >= 4) - throw std::runtime_error("skill usage type out of range"); - if (usageType >= 0) - { - skillGain = skill->mData.mUseValue[usageType]; - if (skillGain < 0) - throw std::runtime_error("invalid skill gain factor"); - } - skillGain *= extraFactor; - - MWMechanics::SkillValue& value = getSkill(skill->mId); - - value.setProgress(value.getProgress() + skillGain); - - if (int(value.getProgress()) >= int(getSkillProgressRequirement(skill->mId, class_))) - { - // skill levelled up - increaseSkill(skill->mId, class_, false); - } -} - -void MWMechanics::NpcStats::increaseSkill(ESM::RefId id, const ESM::Class& class_, bool preserveProgress, bool readBook) -{ - const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().find(id); - float base = getSkill(skill->mId).getBase(); - - if (base >= 100.f) - return; - - base += 1; - - const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); - - // is this a minor or major skill? - int increase = gmst.find("iLevelupMiscMultAttriubte")->mValue.getInteger(); // Note: GMST has a typo - int index = ESM::Skill::refIdToIndex(skill->mId); - for (const auto& skills : class_.mData.mSkills) - { - if (skills[0] == index) - { - mLevelProgress += gmst.find("iLevelUpMinorMult")->mValue.getInteger(); - increase = gmst.find("iLevelUpMinorMultAttribute")->mValue.getInteger(); - break; - } - else if (skills[1] == index) - { - mLevelProgress += gmst.find("iLevelUpMajorMult")->mValue.getInteger(); - increase = gmst.find("iLevelUpMajorMultAttribute")->mValue.getInteger(); - break; - } - } - - mSkillIncreases[ESM::Attribute::indexToRefId(skill->mData.mAttribute)] += increase; - - mSpecIncreases[skill->mData.mSpecialization] += gmst.find("iLevelupSpecialization")->mValue.getInteger(); - - // Play sound & skill progress notification - /// \todo check if character is the player, if levelling is ever implemented for NPCs - MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("skillraise")); - - std::string message{ MWBase::Environment::get().getWindowManager()->getGameSettingString("sNotifyMessage39", {}) }; - message = Misc::StringUtils::format( - message, MyGUI::TextIterator::toTagsString(skill->mName).asUTF8(), static_cast(base)); - - if (readBook) - message = "#{sBookSkillMessage}\n" + message; - - MWBase::Environment::get().getWindowManager()->messageBox(message, MWGui::ShowInDialogueMode_Never); - - if (mLevelProgress >= gmst.find("iLevelUpTotal")->mValue.getInteger()) - { - // levelup is possible now - MWBase::Environment::get().getWindowManager()->messageBox("#{sLevelUpMsg}", MWGui::ShowInDialogueMode_Never); - } - - getSkill(skill->mId).setBase(base); - if (!preserveProgress) - getSkill(skill->mId).setProgress(0); -} - int MWMechanics::NpcStats::getLevelProgress() const { return mLevelProgress; } +void MWMechanics::NpcStats::setLevelProgress(int progress) +{ + mLevelProgress = progress; +} + void MWMechanics::NpcStats::levelUp() { const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); @@ -325,11 +265,33 @@ int MWMechanics::NpcStats::getLevelupAttributeMultiplier(ESM::Attribute::Attribu return MWBase::Environment::get().getESMStore()->get().find(gmst.str())->mValue.getInteger(); } -int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(int spec) const +int MWMechanics::NpcStats::getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const +{ + auto it = mSkillIncreases.find(attribute); + if (it == mSkillIncreases.end()) + return 0; + return it->second; +} + +void MWMechanics::NpcStats::setSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute, int increases) +{ + if (increases == 0) + mSkillIncreases.erase(attribute); + else + mSkillIncreases[attribute] = increases; +} + +int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(ESM::Class::Specialization spec) const { return mSpecIncreases[spec]; } +void MWMechanics::NpcStats::setSkillIncreasesForSpecialization(ESM::Class::Specialization spec, int increases) +{ + assert(spec >= 0 && spec < 3); + mSpecIncreases[spec] = increases; +} + void MWMechanics::NpcStats::flagAsUsed(const ESM::RefId& id) { mUsedIds.insert(id); @@ -461,6 +423,7 @@ void MWMechanics::NpcStats::writeState(ESM::NpcStats& state) const state.mFactions[iter->first].mRank = iter->second; state.mDisposition = mDisposition; + state.mCrimeDispositionModifier = mCrimeDispositionModifier; for (const auto& [id, value] : mSkills) { @@ -488,7 +451,12 @@ void MWMechanics::NpcStats::writeState(ESM::NpcStats& state) const state.mSkillIncrease.fill(0); for (const auto& [key, value] : mSkillIncreases) - state.mSkillIncrease[static_cast(ESM::Attribute::refIdToIndex(key))] = value; + { + // TODO extend format + auto index = ESM::Attribute::refIdToIndex(key); + assert(index >= 0); + state.mSkillIncrease[static_cast(index)] = value; + } for (size_t i = 0; i < state.mSpecIncreases.size(); ++i) state.mSpecIncreases[i] = mSpecIncreases[i]; @@ -520,6 +488,7 @@ void MWMechanics::NpcStats::readState(const ESM::NpcStats& state) } mDisposition = state.mDisposition; + mCrimeDispositionModifier = state.mCrimeDispositionModifier; for (size_t i = 0; i < state.mSkills.size(); ++i) { diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index cae94414c9..f94744cb71 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -3,6 +3,7 @@ #include "creaturestats.hpp" #include +#include #include #include #include @@ -22,6 +23,7 @@ namespace MWMechanics class NpcStats : public CreatureStats { int mDisposition; + int mCrimeDispositionModifier; std::map mSkills; // SkillValue.mProgress used by the player only int mReputation; @@ -54,6 +56,10 @@ namespace MWMechanics int getBaseDisposition() const; void setBaseDisposition(int disposition); + int getCrimeDispositionModifier() const; + void setCrimeDispositionModifier(int value); + void modCrimeDispositionModifier(int value); + int getReputation() const; void setReputation(int reputation); @@ -74,23 +80,22 @@ namespace MWMechanics const std::set& getExpelled() const { return mExpelled; } bool getExpelled(const ESM::RefId& factionID) const; - void expell(const ESM::RefId& factionID); + void expell(const ESM::RefId& factionID, bool printMessage); void clearExpelled(const ESM::RefId& factionID); bool isInFaction(const ESM::RefId& faction) const; float getSkillProgressRequirement(ESM::RefId id, const ESM::Class& class_) const; - void useSkill(ESM::RefId id, const ESM::Class& class_, int usageType = -1, float extraFactor = 1.f); - ///< Increase skill by usage. - - void increaseSkill(ESM::RefId id, const ESM::Class& class_, bool preserveProgress, bool readBook = false); - int getLevelProgress() const; + void setLevelProgress(int progress); int getLevelupAttributeMultiplier(ESM::Attribute::AttributeID attribute) const; + int getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const; + void setSkillIncreasesForAttribute(ESM::Attribute::AttributeID, int increases); - int getSkillIncreasesForSpecialization(int spec) const; + int getSkillIncreasesForSpecialization(ESM::Class::Specialization spec) const; + void setSkillIncreasesForSpecialization(ESM::Class::Specialization spec, int increases); void levelUp(); diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index ab981dd459..12d342666b 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -99,12 +99,12 @@ namespace MWMechanics } bool Objects::playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { - return iter->second->playGroup(groupName, mode, number, persist); + return iter->second->playGroup(groupName, mode, number, scripted); } else { @@ -113,6 +113,23 @@ namespace MWMechanics return false; } } + + bool Objects::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->playGroupLua(groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Objects::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->enableLuaAnimations(enable); + } + void Objects::skipAnimation(const MWWorld::Ptr& ptr) { const auto iter = mIndex.find(ptr.mRef); @@ -126,6 +143,13 @@ namespace MWMechanics object.persistAnimationState(); } + void Objects::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->clearAnimQueue(clearScripted); + } + void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { for (const CharacterController& object : mObjects) diff --git a/apps/openmw/mwmechanics/objects.hpp b/apps/openmw/mwmechanics/objects.hpp index 8b5962109c..31c2768b1b 100644 --- a/apps/openmw/mwmechanics/objects.hpp +++ b/apps/openmw/mwmechanics/objects.hpp @@ -46,9 +46,13 @@ namespace MWMechanics void onClose(const MWWorld::Ptr& ptr); bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool persist = false); + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted = false); + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 4afaf7c2d5..a6eb4f9c24 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -49,47 +49,61 @@ namespace MWMechanics return true; } - const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) + struct GetNearbyDoorVisitor { - MWWorld::CellStore* cell = actor.getCell(); + MWWorld::Ptr mResult; - // Check all the doors in this cell - const MWWorld::CellRefList& doors = cell->getReadOnlyDoors(); - osg::Vec3f pos(actor.getRefData().getPosition().asVec3()); - pos.z() = 0; - - osg::Vec3f actorDir = (actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0)); - - for (const auto& ref : doors.mList) + GetNearbyDoorVisitor(const MWWorld::Ptr& actor, const float minDist) + : mPos(actor.getRefData().getPosition().asVec3()) + , mDir(actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0)) + , mMinDist(minDist) { + mPos.z() = 0; + mDir.normalize(); + } + + bool operator()(const MWWorld::Ptr& ptr) + { + MWWorld::LiveCellRef& ref = *static_cast*>(ptr.getBase()); + if (!ptr.getRefData().isEnabled() || ref.isDeleted()) + return true; + + if (ptr.getClass().getDoorState(ptr) != MWWorld::DoorState::Idle) + return true; + + const float doorRot = ref.mData.getPosition().rot[2] - ptr.getCellRef().getPosition().rot[2]; + if (doorRot != 0) + return true; + osg::Vec3f doorPos(ref.mData.getPosition().asVec3()); - - // FIXME: cast - const MWWorld::Ptr doorPtr - = MWWorld::Ptr(&const_cast&>(ref), actor.getCell()); - - const auto doorState = doorPtr.getClass().getDoorState(doorPtr); - float doorRot = ref.mData.getPosition().rot[2] - doorPtr.getCellRef().getPosition().rot[2]; - - if (doorState != MWWorld::DoorState::Idle || doorRot != 0) - continue; // the door is already opened/opening - doorPos.z() = 0; - float angle = std::acos(actorDir * (doorPos - pos) / (actorDir.length() * (doorPos - pos).length())); + osg::Vec3f actorToDoor = doorPos - mPos; + // Door is not close enough + if (actorToDoor.length2() > mMinDist * mMinDist) + return true; + + actorToDoor.normalize(); + const float angle = std::acos(mDir * actorToDoor); // Allow 60 degrees angle between actor and door if (angle < -osg::PI / 3 || angle > osg::PI / 3) - continue; + return true; - // Door is not close enough - if ((pos - doorPos).length2() > minDist * minDist) - continue; - - return doorPtr; // found, stop searching + mResult = ptr; + return false; // found, stop searching } - return MWWorld::Ptr(); // none found + private: + osg::Vec3f mPos, mDir; + float mMinDist; + }; + + const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) + { + GetNearbyDoorVisitor visitor(actor, minDist); + actor.getCell()->forEachType(visitor); + return visitor.mResult; } bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 44566d3b45..0f688686cd 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include namespace MWWorld diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index 8282e32e81..980c0c660f 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -1,5 +1,6 @@ #include "pathgrid.hpp" +#include #include #include diff --git a/apps/openmw/mwmechanics/recharge.cpp b/apps/openmw/mwmechanics/recharge.cpp index fc8a0e8a72..7b0ad75d3c 100644 --- a/apps/openmw/mwmechanics/recharge.cpp +++ b/apps/openmw/mwmechanics/recharge.cpp @@ -38,12 +38,14 @@ namespace MWMechanics bool rechargeItem(const MWWorld::Ptr& item, const MWWorld::Ptr& gem) { - if (!gem.getRefData().getCount()) + if (!gem.getCellRef().getCount()) return false; MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + MWBase::Environment::get().getWorld()->breakInvisibility(player); + float luckTerm = 0.1f * stats.getAttribute(ESM::Attribute::Luck).getModified(); if (luckTerm < 1 || luckTerm > 10) luckTerm = 1; @@ -82,10 +84,10 @@ namespace MWMechanics MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Enchant Fail")); } - player.getClass().skillUsageSucceeded(player, ESM::Skill::Enchant, 0); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Enchant, ESM::Skill::Enchant_Recharge); gem.getContainerStore()->remove(gem, 1); - if (gem.getRefData().getCount() == 0) + if (gem.getCellRef().getCount() == 0) { std::string message = MWBase::Environment::get() .getESMStore() diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 4894b93e7f..914fa0b542 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -22,6 +22,8 @@ namespace MWMechanics MWWorld::Ptr player = getPlayer(); MWWorld::LiveCellRef* ref = mTool.get(); + MWBase::Environment::get().getWorld()->breakInvisibility(player); + // unstack tool if required player.getClass().getContainerStore(player).unstack(mTool); @@ -68,7 +70,7 @@ namespace MWMechanics stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); // increase skill - player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, ESM::Skill::Armorer_Repair); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Repair")); MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairSuccess}"); diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index a13131cae6..0fb8a95699 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -64,7 +64,7 @@ namespace MWMechanics lock.getCellRef().unlock(); resultMessage = "#{sLockSuccess}"; resultSound = "Open Lock"; - mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1); + mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_PickLock); } else resultMessage = "#{sLockFail}"; @@ -115,7 +115,7 @@ namespace MWMechanics resultSound = "Disarm Trap"; resultMessage = "#{sTrapSuccess}"; - mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0); + mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_DisarmTrap); } else resultMessage = "#{sTrapFail}"; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 52e371b6e9..dd3892e2d9 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -29,11 +29,11 @@ namespace MWMechanics { CastSpell::CastSpell( - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool manualSpell) + const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool scriptedSpell) : mCaster(caster) , mTarget(target) , mFromProjectile(fromProjectile) - , mManualSpell(manualSpell) + , mScriptedSpell(scriptedSpell) { } @@ -41,22 +41,19 @@ namespace MWMechanics const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const { const auto world = MWBase::Environment::get().getWorld(); - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - std::map> toApply; - int index = -1; - for (const ESM::ENAMstruct& effectInfo : effects.mList) + std::map> toApply; + for (const ESM::IndexedENAMstruct& effectInfo : effects.mList) { - ++index; - const ESM::MagicEffect* effect = world->getStore().get().find(effectInfo.mEffectID); + const ESM::MagicEffect* effect = world->getStore().get().find(effectInfo.mData.mEffectID); - if (effectInfo.mRange != rangeType - || (effectInfo.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) + if (effectInfo.mData.mRange != rangeType + || (effectInfo.mData.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) continue; // Not right range type, or not area effect and hit an actor - if (mFromProjectile && effectInfo.mArea <= 0) + if (mFromProjectile && effectInfo.mData.mArea <= 0) continue; // Don't play explosion for projectiles with 0-area effects - if (!mFromProjectile && effectInfo.mRange == ESM::RT_Touch && !ignore.isEmpty() + if (!mFromProjectile && effectInfo.mData.mRange == ESM::RT_Touch && !ignore.isEmpty() && !ignore.getClass().isActor() && !ignore.getClass().hasToolTip(ignore) && (mCaster.isEmpty() || mCaster.getClass().isActor())) continue; // Don't play explosion for touch spells on non-activatable objects except when spell is from @@ -71,16 +68,17 @@ namespace MWMechanics const std::string& texture = effect->mParticle; - if (effectInfo.mArea <= 0) + if (effectInfo.mData.mArea <= 0) { - if (effectInfo.mRange == ESM::RT_Target) + if (effectInfo.mData.mRange == ESM::RT_Target) world->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel, vfs), texture, mHitPosition, 1.0f); + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(areaStatic->mModel)), texture, + mHitPosition, 1.0f); continue; } else - world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel, vfs), texture, - mHitPosition, static_cast(effectInfo.mArea * 2)); + world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(areaStatic->mModel)), + texture, mHitPosition, static_cast(effectInfo.mData.mArea * 2)); // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) { @@ -96,7 +94,7 @@ namespace MWMechanics std::vector objects; static const int unitsPerFoot = ceil(Constants::UnitsPerFoot); MWBase::Environment::get().getMechanicsManager()->getObjectsInRange( - mHitPosition, static_cast(effectInfo.mArea * unitsPerFoot), objects); + mHitPosition, static_cast(effectInfo.mData.mArea * unitsPerFoot), objects); for (const MWWorld::Ptr& affected : objects) { // Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing @@ -105,13 +103,6 @@ namespace MWMechanics continue; auto& list = toApply[affected]; - while (list.size() < static_cast(index)) - { - // Insert dummy effects to preserve indices - auto& dummy = list.emplace_back(effectInfo); - dummy.mRange = ESM::RT_Self; - assert(dummy.mRange != rangeType); - } list.push_back(effectInfo); } } @@ -152,45 +143,34 @@ namespace MWMechanics void CastSpell::inflict( const MWWorld::Ptr& target, const ESM::EffectList& effects, ESM::RangeType range, bool exploded) const { + bool targetIsDeadActor = false; const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) { - // Early-out for characters that have departed. const auto& stats = target.getClass().getCreatureStats(target); if (stats.isDead() && stats.isDeathAnimationFinished()) - return; + targetIsDeadActor = true; } // If none of the effects need to apply, we can early-out bool found = false; bool containsRecastable = false; - std::vector magicEffects; - magicEffects.reserve(effects.mList.size()); const auto& store = MWBase::Environment::get().getESMStore()->get(); - for (const ESM::ENAMstruct& effect : effects.mList) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - if (effect.mRange == range) + if (effect.mData.mRange == range) { found = true; - const ESM::MagicEffect* magicEffect = store.find(effect.mEffectID); - // caster needs to be an actor for linked effects (e.g. Absorb) - if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked - && (mCaster.isEmpty() || !mCaster.getClass().isActor())) - { - magicEffects.push_back(nullptr); - continue; - } + const ESM::MagicEffect* magicEffect = store.find(effect.mData.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable)) containsRecastable = true; - magicEffects.push_back(magicEffect); } - else - magicEffects.push_back(nullptr); } if (!found) return; - ActiveSpells::ActiveSpellParams params(*this, mCaster); + ActiveSpells::ActiveSpellParams params(mCaster, mId, mSourceName, mItem); + params.setFlag(mFlags); bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer()); const ActiveSpells* targetSpells = nullptr; @@ -205,31 +185,35 @@ namespace MWMechanics return; } - for (size_t currentEffectIndex = 0; !target.isEmpty() && currentEffectIndex < effects.mList.size(); - ++currentEffectIndex) + for (auto& enam : effects.mList) { - const ESM::ENAMstruct& enam = effects.mList[currentEffectIndex]; - if (enam.mRange != range) - continue; + if (target.isEmpty()) + break; - const ESM::MagicEffect* magicEffect = magicEffects[currentEffectIndex]; + if (enam.mData.mRange != range) + continue; + const ESM::MagicEffect* magicEffect = store.find(enam.mData.mEffectID); if (!magicEffect) continue; + // caster needs to be an actor for linked effects (e.g. Absorb) + if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked + && (mCaster.isEmpty() || !mCaster.getClass().isActor())) + continue; ActiveSpells::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; effect.mTimeLeft = 0.f; - effect.mEffectIndex = static_cast(currentEffectIndex); + effect.mEffectIndex = enam.mIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; - if (mManualSpell) + if (mScriptedSpell) effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(enam.mDuration) : 1.f; + effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; if (!appliedOnce) @@ -241,8 +225,8 @@ namespace MWMechanics params.getEffects().emplace_back(effect); bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful - || enam.mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != mCaster && targetIsActor && effectAffectsHealth) + || enam.mData.mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != mCaster && targetIsActor && !targetIsDeadActor && effectAffectsHealth) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's // HP bar. @@ -263,7 +247,10 @@ namespace MWMechanics if (!params.getEffects().empty()) { if (targetIsActor) - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); + { + if (!targetIsDeadActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); + } else { // Apply effects instantly. We can ignore effect deletion since the entire params object gets @@ -337,7 +324,7 @@ namespace MWMechanics ESM::RefId school = ESM::Skill::Alteration; if (!enchantment->mEffects.mList.empty()) { - short effectId = enchantment->mEffects.mList.front().mEffectID; + short effectId = enchantment->mEffects.mList.front().mData.mEffectID; const ESM::MagicEffect* magicEffect = store->get().find(effectId); school = magicEffect->mData.mSchool; } @@ -355,7 +342,7 @@ namespace MWMechanics if (type == ESM::Enchantment::WhenUsed) { if (mCaster == getPlayer()) - mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, 1); + mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_UseMagicItem); } else if (type == ESM::Enchantment::CastOnce) { @@ -365,7 +352,7 @@ namespace MWMechanics else if (type == ESM::Enchantment::WhenStrikes) { if (mCaster == getPlayer()) - mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, 3); + mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_CastOnStrike); } if (isProjectile) @@ -388,9 +375,13 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mType = ESM::ActiveSpells::Type_Consumable; + mFlags = static_cast( + ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); - inflict(mCaster, potion->mEffects, ESM::RT_Self); + // Ignore range and don't apply area of effect + inflict(mCaster, potion->mEffects, ESM::RT_Self, true); + inflict(mCaster, potion->mEffects, ESM::RT_Touch, true); + inflict(mCaster, potion->mEffects, ESM::RT_Target, true); return true; } @@ -404,7 +395,7 @@ namespace MWMechanics bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mManualSpell) + if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mScriptedSpell) { school = getSpellSchool(spell, mCaster); @@ -439,8 +430,8 @@ namespace MWMechanics stats.getSpells().usePower(spell); } - if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) - mCaster.getClass().skillUsageSucceeded(mCaster, school, 0); + if (!mScriptedSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) + mCaster.getClass().skillUsageSucceeded(mCaster, school, ESM::Skill::Spellcast_Success); // A non-actor doesn't play its spell cast effects from a character controller, so play them here if (!mCaster.getClass().isActor()) @@ -459,62 +450,27 @@ namespace MWMechanics bool CastSpell::cast(const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mType = ESM::ActiveSpells::Type_Consumable; + mFlags = static_cast( + ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); mSourceName = ingredient->mName; - ESM::ENAMstruct effect; - effect.mEffectID = ingredient->mData.mEffectID[0]; - effect.mSkill = ingredient->mData.mSkills[0]; - effect.mAttribute = ingredient->mData.mAttributes[0]; - effect.mRange = ESM::RT_Self; - effect.mArea = 0; + auto effect = rollIngredientEffect(mCaster, ingredient, mCaster != getPlayer()); - const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const auto magicEffect = store.get().find(effect.mEffectID); - const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster); - - float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) - + 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() - + 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()) - * creatureStats.getFatigueTerm(); - - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int roll = Misc::Rng::roll0to99(prng); - if (roll > x) + if (effect) + inflict(mCaster, *effect, ESM::RT_Self); + else { // "X has no effect on you" - std::string message = store.get().find("sNotifyMessage50")->mValue.getString(); + std::string message = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sNotifyMessage50") + ->mValue.getString(); message = Misc::StringUtils::format(message, ingredient->mName); MWBase::Environment::get().getWindowManager()->messageBox(message); return false; } - float magnitude = 0; - float y = roll / std::min(x, 100.f); - y *= 0.25f * x; - if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - effect.mDuration = 1; - else - effect.mDuration = static_cast(y); - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) - { - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); - else - magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); - magnitude = std::max(1.f, magnitude); - } - else - magnitude = 1; - - effect.mMagnMax = static_cast(magnitude); - effect.mMagnMin = static_cast(magnitude); - - ESM::EffectList effects; - effects.mList.push_back(effect); - - inflict(mCaster, effects, ESM::RT_Self); - return true; } @@ -528,15 +484,14 @@ namespace MWMechanics playSpellCastingEffects(spell->mEffects.mList); } - void CastSpell::playSpellCastingEffects(const std::vector& effects) const + void CastSpell::playSpellCastingEffects(const std::vector& effects) const { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - std::vector addedEffects; - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + std::vector addedEffects; - for (const ESM::ENAMstruct& effectData : effects) + for (const ESM::IndexedENAMstruct& effectData : effects) { - const auto effect = store.get().find(effectData.mEffectID); + const auto effect = store.get().find(effectData.mData.mEffectID); const ESM::Static* castStatic; @@ -545,17 +500,18 @@ namespace MWMechanics else castStatic = store.get().find(ESM::RefId::stringRefId("VFX_DefaultCast")); + VFS::Path::Normalized castStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(castStatic->mModel)); + // check if the effect was already added - if (std::find(addedEffects.begin(), addedEffects.end(), - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs)) - != addedEffects.end()) + if (std::find(addedEffects.begin(), addedEffects.end(), castStaticModel) != addedEffects.end()) continue; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), effect->mIndex, - false, {}, effect->mParticle); + animation->addEffect(castStaticModel.value(), ESM::MagicEffect::indexToName(effect->mIndex), false, {}, + effect->mParticle); } else { @@ -584,14 +540,13 @@ namespace MWMechanics scale *= npcScaleVec.z(); } scale = std::max(scale, 1.f); - MWBase::Environment::get().getWorld()->spawnEffect( - Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), effect->mParticle, pos, scale); + MWBase::Environment::get().getWorld()->spawnEffect(castStaticModel, effect->mParticle, pos, scale); } if (animation && !mCaster.getClass().isActor()) - animation->addSpellCastGlow(effect); + animation->addSpellCastGlow(effect->getColor()); - addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs)); + addedEffects.push_back(std::move(castStaticModel)); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (!effect->mCastSound.empty()) @@ -629,9 +584,10 @@ namespace MWMechanics // Don't play particle VFX unless the effect is new or it should be looping. if (playNonLooping || loop) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), magicEffect.mIndex, - loop, {}, magicEffect.mParticle); + const VFS::Path::Normalized castStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(castStatic->mModel)); + anim->addEffect(castStaticModel.value(), ESM::MagicEffect::indexToName(magicEffect.mIndex), loop, {}, + magicEffect.mParticle); } } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 8f10066e04..23d3b80713 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -26,7 +26,7 @@ namespace MWMechanics MWWorld::Ptr mCaster; // May be empty MWWorld::Ptr mTarget; // May be empty - void playSpellCastingEffects(const std::vector& effects) const; + void playSpellCastingEffects(const std::vector& effects) const; void explodeSpell(const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const; @@ -41,13 +41,13 @@ namespace MWMechanics false }; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) - bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, - // etc.) + bool mScriptedSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, + // etc.) ESM::RefNum mItem; - ESM::ActiveSpells::EffectType mType{ ESM::ActiveSpells::Type_Temporary }; + ESM::ActiveSpells::Flags mFlags{ ESM::ActiveSpells::Flag_Temporary }; CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile = false, - const bool manualSpell = false); + const bool scriptedSpell = false); bool cast(const ESM::Spell* spell); diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 253c4015b5..7035c7f61c 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -279,26 +279,28 @@ namespace return false; } - void absorbSpell(const ESM::RefId& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) + void absorbSpell(const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, const MWWorld::Ptr& caster, + const MWWorld::Ptr& target) { const auto& esmStore = *MWBase::Environment::get().getESMStore(); const ESM::Static* absorbStatic = esmStore.get().find(ESM::RefId::stringRefId("VFX_Absorb")); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !absorbStatic->mModel.empty()) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel, vfs), - ESM::MagicEffect::SpellAbsorption, false); + const VFS::Path::Normalized absorbStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(absorbStatic->mModel)); + animation->addEffect( + absorbStaticModel.value(), ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false); } - const ESM::Spell* spell = esmStore.get().search(spellId); + int spellCost = 0; - if (spell) + if (const ESM::Spell* spell = esmStore.get().search(spellParams.getSourceSpellId())) { spellCost = MWMechanics::calcSpellCost(*spell); } else { - const ESM::Enchantment* enchantment = esmStore.get().search(spellId); + const ESM::Enchantment* enchantment = esmStore.get().search(spellParams.getEnchantment()); if (enchantment) spellCost = MWMechanics::getEffectiveEnchantmentCastCost(*enchantment, caster); } @@ -317,8 +319,7 @@ namespace auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects(); // Apply reflect and spell absorption - if (target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment - && spellParams.getType() != ESM::ActiveSpells::Type_Permanent) + if (target != caster && spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)) { bool canReflect = !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) && !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) @@ -345,7 +346,7 @@ namespace { if (canAbsorb && Misc::Rng::roll0to99(prng) < activeEffect.mMagnitude) { - absorbSpell(spellParams.getId(), caster, target); + absorbSpell(spellParams, caster, target); return MWMechanics::MagicApplicationResult::Type::REMOVED; } } @@ -356,13 +357,13 @@ namespace // Notify the target actor they've been hit bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); + target.getClass().onHit( + target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical); // Apply resistances if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) { - const ESM::Spell* spell = nullptr; - if (spellParams.getType() == ESM::ActiveSpells::Type_Temporary) - spell = MWBase::Environment::get().getESMStore()->get().search(spellParams.getId()); + const ESM::Spell* spell + = spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary) ? spellParams.getSpell() : nullptr; float magnitudeMult = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); if (magnitudeMult == 0) @@ -431,10 +432,9 @@ namespace MWMechanics // Dispel removes entire spells at once target.getClass().getCreatureStats(target).getActiveSpells().purge( [magnitude = effect.mMagnitude](const ActiveSpells::ActiveSpellParams& params) { - if (params.getType() == ESM::ActiveSpells::Type_Temporary) + if (params.hasFlag(ESM::ActiveSpells::Flag_Temporary)) { - const ESM::Spell* spell - = MWBase::Environment::get().getESMStore()->get().search(params.getId()); + const ESM::Spell* spell = params.getSpell(); if (spell && spell->mData.mType == ESM::Spell::ST_Spell) { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); @@ -457,13 +457,14 @@ namespace MWMechanics if (!caster.isEmpty()) { MWRender::Animation* anim = world->getAnimation(caster); - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_end")); - if (fx) + if (fx != nullptr) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), -1); + const VFS::Path::Normalized fxModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)); + anim->addEffect(fxModel.value(), ""); } } } @@ -495,7 +496,7 @@ namespace MWMechanics if (!caster.isEmpty()) { MWRender::Animation* anim = world->getAnimation(caster); - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } } } @@ -559,6 +560,10 @@ namespace MWMechanics modifyAiSetting( target, effect, ESM::MagicEffect::RallyCreature, AiSetting::Flee, -effect.mMagnitude, invalid); break; + case ESM::MagicEffect::Charm: + if (!target.getClass().isNpc()) + invalid = true; + break; case ESM::MagicEffect::Sound: if (target == getPlayer()) { @@ -646,7 +651,7 @@ namespace MWMechanics else if (effect.mEffectId == ESM::MagicEffect::DamageFatigue) index = 2; // Damage "Dynamic" abilities reduce the base value - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) modDynamicStat(target, index, -effect.mMagnitude); else { @@ -667,7 +672,7 @@ namespace MWMechanics else if (!godmode) { // Damage Skill abilities reduce base skill :todd: - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& npcStats = target.getClass().getNpcStats(target); SkillValue& skill = npcStats.getSkill(effect.getSkillOrAttribute()); @@ -717,8 +722,8 @@ namespace MWMechanics if (!godmode) { int index = effect.mEffectId - ESM::MagicEffect::DrainHealth; - adjustDynamicStat( - target, index, -effect.mMagnitude, Settings::game().mUncappedDamageFatigue && index == 2); + // Unlike Absorb and Damage effects Drain effects can bring stats below zero + adjustDynamicStat(target, index, -effect.mMagnitude, true); if (index == 0) receivedMagicDamage = affectedHealth = true; } @@ -726,7 +731,7 @@ namespace MWMechanics case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyFatigue: - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); else adjustDynamicStat( @@ -738,7 +743,7 @@ namespace MWMechanics break; case ESM::MagicEffect::FortifyAttribute: // Abilities affect base stats, but not for drain - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& creatureStats = target.getClass().getCreatureStats(target); auto attribute = effect.getSkillOrAttribute(); @@ -758,7 +763,7 @@ namespace MWMechanics case ESM::MagicEffect::FortifySkill: if (!target.getClass().isNpc()) invalid = true; - else if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + else if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { // Abilities affect base stats, but not for drain auto& npcStats = target.getClass().getNpcStats(target); @@ -923,11 +928,14 @@ namespace MWMechanics { MWRender::Animation* animation = world->getAnimation(target); if (animation) - animation->addSpellCastGlow(magicEffect); + animation->addSpellCastGlow(magicEffect->getColor()); int magnitude = static_cast(roll(effect)); if (target.getCellRef().getLockLevel() < magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude { + MWBase::Environment::get().getSoundManager()->playSound3D( + target, ESM::RefId::stringRefId("Open Lock"), 1.f, 1.f); + if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); target.getCellRef().lock(magnitude); @@ -945,7 +953,7 @@ namespace MWMechanics MWRender::Animation* animation = world->getAnimation(target); if (animation) - animation->addSpellCastGlow(magicEffect); + animation->addSpellCastGlow(magicEffect->getColor()); int magnitude = static_cast(roll(effect)); if (target.getCellRef().getLockLevel() <= magnitude) { @@ -983,7 +991,7 @@ namespace MWMechanics return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects(); - if (spellParams.getType() != ESM::ActiveSpells::Type_Ability + if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues) && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) { MagicApplicationResult::Type result @@ -996,10 +1004,9 @@ namespace MWMechanics oldMagnitude = effect.mMagnitude; else { - if (spellParams.getType() != ESM::ActiveSpells::Type_Enchantment) - playEffects(target, *magicEffect, - spellParams.getType() == ESM::ActiveSpells::Type_Consumable - || spellParams.getType() == ESM::ActiveSpells::Type_Temporary); + if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_Equipment) + && !spellParams.hasFlag(ESM::ActiveSpells::Flag_Lua)) + playEffects(target, *magicEffect, spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)); if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() && target.getType() == ESM::Creature::sRecordId && target.get()->mBase->mData.mSoul == 0 && caster == getPlayer()) @@ -1014,8 +1021,7 @@ namespace MWMechanics if (effect.mDuration != 0) { float mult = dt; - if (spellParams.getType() == ESM::ActiveSpells::Type_Consumable - || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)) mult = std::min(effect.mTimeLeft, dt); effect.mMagnitude *= mult; } @@ -1043,7 +1049,7 @@ namespace MWMechanics effect.mFlags |= ESM::ActiveEffect::Flag_Remove; auto anim = world->getAnimation(target); if (anim) - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } else effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; @@ -1193,7 +1199,7 @@ namespace MWMechanics case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyFatigue: - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); else adjustDynamicStat( @@ -1204,7 +1210,7 @@ namespace MWMechanics break; case ESM::MagicEffect::FortifyAttribute: // Abilities affect base stats, but not for drain - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& creatureStats = target.getClass().getCreatureStats(target); auto attribute = effect.getSkillOrAttribute(); @@ -1220,7 +1226,7 @@ namespace MWMechanics break; case ESM::MagicEffect::FortifySkill: // Abilities affect base stats, but not for drain - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& npcStats = target.getClass().getNpcStats(target); auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); @@ -1285,7 +1291,7 @@ namespace MWMechanics { auto anim = MWBase::Environment::get().getWorld()->getAnimation(target); if (anim) - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } } diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index 776381e6b2..7041254c4e 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -34,7 +34,7 @@ namespace if (effectFilter == -1) { const ESM::Spell* spell - = MWBase::Environment::get().getESMStore()->get().search(it->getId()); + = MWBase::Environment::get().getESMStore()->get().search(it->getSourceSpellId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } @@ -67,7 +67,7 @@ namespace const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->getId() != spellId) + if (it->getSourceSpellId() != spellId) continue; const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; @@ -85,7 +85,7 @@ namespace int actorId = caster.getClass().getCreatureStats(caster).getActorId(); const auto& active = target.getClass().getCreatureStats(target).getActiveSpells(); return std::find_if(active.begin(), active.end(), [&](const auto& spell) { - return spell.getCasterActorId() == actorId && spell.getId() == id; + return spell.getCasterActorId() == actorId && spell.getSourceSpellId() == id; }) != active.end(); } @@ -110,13 +110,13 @@ namespace MWMechanics int getRangeTypes(const ESM::EffectList& effects) { int types = 0; - for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - if (it->mRange == ESM::RT_Self) + if (effect.mData.mRange == ESM::RT_Self) types |= RangeTypes::Self; - else if (it->mRange == ESM::RT_Touch) + else if (effect.mData.mRange == ESM::RT_Touch) types |= RangeTypes::Touch; - else if (it->mRange == ESM::RT_Target) + else if (effect.mData.mRange == ESM::RT_Target) types |= RangeTypes::Target; } return types; @@ -440,12 +440,12 @@ namespace MWMechanics // NB: this currently assumes the hardcoded magic effect flags are used const float magnitude = (effect.mMagnMin + effect.mMagnMax) / 2.f; const float toHeal = magnitude * std::max(1, effect.mDuration); - const float damage = current.getModified() - current.getCurrent(); + const float damage = std::max(current.getModified() - current.getCurrent(), 0.f); float priority = 0.f; if (effect.mEffectID == ESM::MagicEffect::RestoreHealth) priority = 4.f; else if (effect.mEffectID == ESM::MagicEffect::RestoreMagicka) - priority = std::max(0.1f, getRestoreMagickaPriority(actor)); + priority = getRestoreMagickaPriority(actor); else if (effect.mEffectID == ESM::MagicEffect::RestoreFatigue) priority = 2.f; float overheal = 0.f; @@ -456,9 +456,8 @@ namespace MWMechanics heal = damage; } - priority = (std::pow(priority + heal / current.getModified(), damage * 4.f / current.getModified()) - - priority - overheal / current.getModified()) - / priority; + priority = (priority - 1.f) / 2.f * std::pow((damage / current.getModified() + 0.6f), priority * 2) + + priority * (heal - 2.f * overheal) / current.getModified() - 0.5f; rating = priority; } break; @@ -735,12 +734,12 @@ namespace MWMechanics static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); - for (const ESM::ENAMstruct& effect : list.mList) + for (const ESM::IndexedENAMstruct& effect : list.mList) { - float effectRating = rateEffect(effect, actor, enemy); + float effectRating = rateEffect(effect.mData, actor, enemy); if (useSpellMult) { - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) effectRating *= fAIRangeMagicSpellMult; else effectRating *= fAIMagicSpellMult; @@ -760,10 +759,10 @@ namespace MWMechanics float mult = fAIMagicSpellMult; - for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { if (!MWBase::Environment::get().getWorld()->isSwimming(enemy)) mult = fAIRangeMagicSpellMult; diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index 12da7cdde8..d2baedab0f 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -60,14 +60,17 @@ namespace MWMechanics return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } - void Spells::add(const ESM::Spell* spell) + void Spells::add(const ESM::Spell* spell, bool modifyBase) { - mSpellList->add(spell); + if (modifyBase) + mSpellList->add(spell); + else + addSpell(spell); } - void Spells::add(const ESM::RefId& spellId) + void Spells::add(const ESM::RefId& spellId, bool modifyBase) { - add(SpellList::getSpell(spellId)); + add(SpellList::getSpell(spellId), modifyBase); } void Spells::addSpell(const ESM::Spell* spell) @@ -76,13 +79,17 @@ namespace MWMechanics mSpells.emplace_back(spell); } - void Spells::remove(const ESM::RefId& spellId) + void Spells::remove(const ESM::RefId& spellId, bool modifyBase) { - const auto spell = SpellList::getSpell(spellId); - removeSpell(spell); - mSpellList->remove(spell); + remove(SpellList::getSpell(spellId), modifyBase); + } - if (spellId == mSelectedSpell) + void Spells::remove(const ESM::Spell* spell, bool modifyBase) + { + removeSpell(spell); + if (modifyBase) + mSpellList->remove(spell); + if (spell->mId == mSelectedSpell) mSelectedSpell = ESM::RefId(); } @@ -174,7 +181,7 @@ namespace MWMechanics { for (const auto& effectIt : spell->mEffects.mList) { - if (effectIt.mEffectID == ESM::MagicEffect::Corprus) + if (effectIt.mData.mEffectID == ESM::MagicEffect::Corprus) { return true; } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 685823b131..36c50b47c3 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -74,24 +74,25 @@ namespace MWMechanics bool hasSpell(const ESM::RefId& spell) const; bool hasSpell(const ESM::Spell* spell) const; - void add(const ESM::RefId& spell); + void add(const ESM::RefId& spell, bool modifyBase = true); ///< Adding a spell that is already listed in *this is a no-op. - void add(const ESM::Spell* spell); + void add(const ESM::Spell* spell, bool modifyBase = true); ///< Adding a spell that is already listed in *this is a no-op. - void remove(const ESM::RefId& spell); + void remove(const ESM::RefId& spell, bool modifyBase = true); + void remove(const ESM::Spell* spell, bool modifyBase = true); ///< If the spell to be removed is the selected spell, the selected spell will be changed to - /// no spell (empty string). + /// no spell (empty id). void clear(bool modifyBase = false); - ///< Remove all spells of al types. + ///< Remove all spells of all types. void setSelectedSpell(const ESM::RefId& spellId); ///< This function does not verify, if the spell is available. const ESM::RefId& getSelectedSpell() const; - ///< May return an empty string. + ///< May return an empty id. bool hasCommonDisease() const; diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 2a63a3a444..022aaec262 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -2,7 +2,9 @@ #include +#include #include +#include #include #include "../mwbase/environment.hpp" @@ -22,13 +24,13 @@ namespace MWMechanics { float cost = 0; - for (const ESM::ENAMstruct& effect : list.mList) + for (const ESM::IndexedENAMstruct& effect : list.mList) { - float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect, nullptr, method)); + float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect.mData, nullptr, method)); // This is applied to the whole spell cost for each effect when // creating spells, but is only applied on the effect itself in TES:CS. - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) effectCost *= 1.5; cost += effectCost; @@ -48,7 +50,7 @@ namespace MWMechanics bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; int minMagn = hasMagnitude ? effect.mMagnMin : 1; int maxMagn = hasMagnitude ? effect.mMagnMax : 1; - if (method != EffectCostMethod::GameEnchantment) + if (method == EffectCostMethod::PlayerSpell || method == EffectCostMethod::GameSpell) { minMagn = std::max(1, minMagn); maxMagn = std::max(1, maxMagn); @@ -57,21 +59,28 @@ namespace MWMechanics if (!appliedOnce) duration = std::max(1, duration); static const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); + static const float iAlchemyMod = store.get().find("iAlchemyMod")->mValue.getFloat(); int durationOffset = 0; int minArea = 0; + float costMult = fEffectCostMult; if (method == EffectCostMethod::PlayerSpell) { durationOffset = 1; minArea = 1; } + else if (method == EffectCostMethod::GamePotion) + { + minArea = 1; + costMult = iAlchemyMod; + } float x = 0.5 * (minMagn + maxMagn); x *= 0.1 * magicEffect->mData.mBaseCost; x *= durationOffset + duration; x += 0.05 * std::max(minArea, effect.mArea) * magicEffect->mData.mBaseCost; - return x * fEffectCostMult; + return x * costMult; } int calcSpellCost(const ESM::Spell& spell) @@ -140,25 +149,93 @@ namespace MWMechanics return enchantment.mData.mCharge; } + int getPotionValue(const ESM::Potion& potion) + { + if (potion.mData.mFlags & ESM::Potion::Autocalc) + { + float cost = getTotalCost(potion.mEffects, EffectCostMethod::GamePotion); + return std::round(cost); + } + return potion.mData.mValue; + } + + std::optional rollIngredientEffect( + MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index) + { + if (index >= 4) + throw std::range_error("Index out of range"); + + ESM::ENAMstruct effect; + effect.mEffectID = ingredient->mData.mEffectID[index]; + effect.mSkill = ingredient->mData.mSkills[index]; + effect.mAttribute = ingredient->mData.mAttributes[index]; + effect.mRange = ESM::RT_Self; + effect.mArea = 0; + + if (effect.mEffectID < 0) + return std::nullopt; + + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const auto magicEffect = store.get().find(effect.mEffectID); + const MWMechanics::CreatureStats& creatureStats = caster.getClass().getCreatureStats(caster); + + float x = (caster.getClass().getSkill(caster, ESM::Skill::Alchemy) + + 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() + + 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()) + * creatureStats.getFatigueTerm(); + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); + if (roll > x) + { + return std::nullopt; + } + + float magnitude = 0; + float y = roll / std::min(x, 100.f); + y *= 0.25f * x; + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + effect.mDuration = 1; + else + effect.mDuration = static_cast(y); + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); + else + magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); + magnitude = std::max(1.f, magnitude); + } + else + magnitude = 1; + + effect.mMagnMax = static_cast(magnitude); + effect.mMagnMin = static_cast(magnitude); + + ESM::EffectList effects; + effects.mList.push_back({ effect, index }); + return effects; + } + float calcSpellBaseSuccessChance(const ESM::Spell* spell, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float y = std::numeric_limits::max(); float lowestSkill = 0; - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList) { - float x = static_cast(effect.mDuration); + float x = static_cast(effect.mData.mDuration); const auto magicEffect - = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) x = std::max(1.f, x); x *= 0.1f * magicEffect->mData.mBaseCost; - x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); - x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; - if (effect.mRange == ESM::RT_Target) + x *= 0.5f * (effect.mData.mMagnMin + effect.mData.mMagnMax); + x += effect.mData.mArea * 0.05f * magicEffect->mData.mBaseCost; + if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; static const float fEffectCostMult = MWBase::Environment::get() .getESMStore() diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp index a332a231e6..fb9d14c8a5 100644 --- a/apps/openmw/mwmechanics/spellutil.hpp +++ b/apps/openmw/mwmechanics/spellutil.hpp @@ -3,11 +3,16 @@ #include +#include + namespace ESM { + struct EffectList; struct ENAMstruct; struct Enchantment; + struct Ingredient; struct MagicEffect; + struct Potion; struct Spell; } @@ -23,6 +28,7 @@ namespace MWMechanics GameSpell, PlayerSpell, GameEnchantment, + GamePotion, }; float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr, @@ -33,6 +39,10 @@ namespace MWMechanics int getEffectiveEnchantmentCastCost(const ESM::Enchantment& enchantment, const MWWorld::Ptr& actor); int getEnchantmentCharge(const ESM::Enchantment& enchantment); + int getPotionValue(const ESM::Potion& potion); + std::optional rollIngredientEffect( + MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index = 0); + /** * @param spell spell to cast * @param actor calculate spell success chance for this actor (depends on actor's skills) diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 2af5a2dc83..78d4976040 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -90,27 +90,24 @@ namespace MWMechanics { auto world = MWBase::Environment::get().getWorld(); MWWorld::ManualRef ref(world->getStore(), creatureID, 1); + MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); - MWMechanics::CreatureStats& summonedCreatureStats - = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); + MWMechanics::CreatureStats& summonedCreatureStats = placed.getClass().getCreatureStats(placed); // Make the summoned creature follow its master and help in fights AiFollow package(summoner); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); + summonedCreatureStats.getAiSequence().stack(package, placed); creatureActorId = summonedCreatureStats.getActorId(); - MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); - MWRender::Animation* anim = world->getAnimation(placed); if (anim) { const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_Start")); if (fx) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel, vfs), -1, false); - } + anim->addEffect( + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)).value(), "", + false); } } catch (std::exception& e) diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp deleted file mode 100644 index 9500897f25..0000000000 --- a/apps/openmw/mwmechanics/trading.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "trading.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - Trading::Trading() {} - - bool Trading::haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer) - { - // accept if merchant offer is better than player offer - if (playerOffer <= merchantOffer) - { - return true; - } - - // reject if npc is a creature - if (merchant.getType() != ESM::NPC::sRecordId) - { - return false; - } - - const MWWorld::Store& gmst - = MWBase::Environment::get().getESMStore()->get(); - - // Is the player buying? - bool buying = (merchantOffer < 0); - int a = std::abs(merchantOffer); - int b = std::abs(playerOffer); - int d = (buying) ? int(100 * (a - b) / a) : int(100 * (b - a) / b); - - int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(merchant); - - const MWMechanics::CreatureStats& merchantStats = merchant.getClass().getCreatureStats(merchant); - const MWMechanics::CreatureStats& playerStats = player.getClass().getCreatureStats(player); - - float a1 = static_cast(player.getClass().getSkill(player, ESM::Skill::Mercantile)); - float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); - float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); - float d1 = static_cast(merchant.getClass().getSkill(merchant, ESM::Skill::Mercantile)); - float e1 = 0.1f * merchantStats.getAttribute(ESM::Attribute::Luck).getModified(); - float f1 = 0.2f * merchantStats.getAttribute(ESM::Attribute::Personality).getModified(); - - float dispositionTerm = gmst.find("fDispositionMod")->mValue.getFloat() * (clampedDisposition - 50); - float pcTerm = (dispositionTerm + a1 + b1 + c1) * playerStats.getFatigueTerm(); - float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); - float x = gmst.find("fBargainOfferMulti")->mValue.getFloat() * d - + gmst.find("fBargainOfferBase")->mValue.getFloat() + int(pcTerm - npcTerm); - - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int roll = Misc::Rng::rollDice(100, prng) + 1; - - // reject if roll fails - // (or if player tries to buy things and get money) - if (roll > x || (merchantOffer < 0 && 0 < playerOffer)) - { - return false; - } - - // apply skill gain on successful barter - float skillGain = 0.f; - int finalPrice = std::abs(playerOffer); - int initialMerchantOffer = std::abs(merchantOffer); - - if (!buying && (finalPrice > initialMerchantOffer)) - { - skillGain = std::floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); - } - else if (buying && (finalPrice < initialMerchantOffer)) - { - skillGain = std::floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); - } - player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); - - return true; - } -} diff --git a/apps/openmw/mwmechanics/trading.hpp b/apps/openmw/mwmechanics/trading.hpp deleted file mode 100644 index e30b82f5e8..0000000000 --- a/apps/openmw/mwmechanics/trading.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef OPENMW_MECHANICS_TRADING_H -#define OPENMW_MECHANICS_TRADING_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class Trading - { - public: - Trading(); - - bool haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer); - }; -} - -#endif diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index e0584afcd4..dd83db286f 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -118,13 +118,17 @@ namespace MWMechanics } int value = 50.f; - if (actor.getClass().isNpc()) - { - ESM::RefId skill = item.getClass().getEquipmentSkill(item); - if (!skill.empty()) - value = actor.getClass().getSkill(actor, skill); - } - else + ESM::RefId skill = item.getClass().getEquipmentSkill(item); + if (!skill.empty()) + value = actor.getClass().getSkill(actor, skill); + // Prefer hand-to-hand if our skill is 0 (presumably due to magic) + if (value <= 0.f) + return 0.f; + // Note that a creature with a dagger and 0 Stealth will forgo the weapon despite using Combat for hit chance. + // The same creature will use a sword provided its Combat stat isn't 0. We're using the "skill" value here to + // decide whether to use the weapon at all, but adjusting the final rating based on actual hit chance - i.e. the + // Combat stat. + if (!actor.getClass().isNpc()) { MWWorld::LiveCellRef* ref = actor.get(); value = ref->mBase->mData.mCombat; diff --git a/apps/openmw/mwmechanics/weapontype.cpp b/apps/openmw/mwmechanics/weapontype.cpp index 9dd5842f58..8c51629803 100644 --- a/apps/openmw/mwmechanics/weapontype.cpp +++ b/apps/openmw/mwmechanics/weapontype.cpp @@ -8,6 +8,8 @@ #include +#include + namespace MWMechanics { template @@ -416,4 +418,18 @@ namespace MWMechanics return &Weapon::getValue(); } + + std::vector getAllWeaponTypeShortGroups() + { + // Go via a set to eliminate duplicates. + std::set shortGroupSet; + for (int type = ESM::Weapon::Type::First; type <= ESM::Weapon::Type::Last; type++) + { + std::string_view shortGroup = getWeaponType(type)->mShortGroup; + if (!shortGroup.empty()) + shortGroupSet.insert(shortGroup); + } + + return std::vector(shortGroupSet.begin(), shortGroupSet.end()); + } } diff --git a/apps/openmw/mwmechanics/weapontype.hpp b/apps/openmw/mwmechanics/weapontype.hpp index db7b3013f6..efe404d327 100644 --- a/apps/openmw/mwmechanics/weapontype.hpp +++ b/apps/openmw/mwmechanics/weapontype.hpp @@ -1,6 +1,9 @@ #ifndef GAME_MWMECHANICS_WEAPONTYPE_H #define GAME_MWMECHANICS_WEAPONTYPE_H +#include +#include + namespace ESM { struct WeaponType; @@ -21,6 +24,8 @@ namespace MWMechanics MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int* weaptype); const ESM::WeaponType* getWeaponType(const int weaponType); + + std::vector getAllWeaponTypeShortGroups(); } #endif diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index dec055d68f..e1efe6d242 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -102,7 +102,11 @@ namespace MWPhysics updateScaleUnsafe(); if (!mRotationallyInvariant) - mRotation = mPtr.getRefData().getBaseNode()->getAttitude(); + { + const SceneUtil::PositionAttitudeTransform* baseNode = mPtr.getRefData().getBaseNode(); + if (baseNode) + mRotation = baseNode->getAttitude(); + } addCollisionMask(getCollisionMask()); updateCollisionObjectPositionUnsafe(); diff --git a/apps/openmw/mwphysics/actorconvexcallback.cpp b/apps/openmw/mwphysics/actorconvexcallback.cpp index db077beb31..72bb0eff46 100644 --- a/apps/openmw/mwphysics/actorconvexcallback.cpp +++ b/apps/openmw/mwphysics/actorconvexcallback.cpp @@ -9,35 +9,28 @@ namespace MWPhysics { - class ActorOverlapTester : public btCollisionWorld::ContactResultCallback + namespace { - public: - bool overlapping = false; - - btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* colObj0Wrap, int partId0, - int index0, const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1) override + struct ActorOverlapTester : public btCollisionWorld::ContactResultCallback { - if (cp.getDistance() <= 0.0f) - overlapping = true; - return btScalar(1); - } - }; + bool mOverlapping = false; - ActorConvexCallback::ActorConvexCallback( - const btCollisionObject* me, const btVector3& motion, btScalar minCollisionDot, const btCollisionWorld* world) - : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)) - , mMe(me) - , mMotion(motion) - , mMinCollisionDot(minCollisionDot) - , mWorld(world) - { + btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* /*colObj0Wrap*/, + int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* /*colObj1Wrap*/, int /*partId1*/, + int /*index1*/) override + { + if (cp.getDistance() <= 0.0f) + mOverlapping = true; + return 1; + } + }; } btScalar ActorConvexCallback::addSingleResult( btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) { if (convexResult.m_hitCollisionObject == mMe) - return btScalar(1); + return 1; // override data for actor-actor collisions // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter @@ -52,7 +45,7 @@ namespace MWPhysics const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), isOverlapping); - if (isOverlapping.overlapping) + if (isOverlapping.mOverlapping) { auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin()); auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin()); @@ -73,7 +66,7 @@ namespace MWPhysics } else { - return btScalar(1); + return 1; } } } @@ -82,10 +75,10 @@ namespace MWPhysics { auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); if (!projectileHolder->isActive()) - return btScalar(1); + return 1; if (projectileHolder->isValidTarget(mMe)) projectileHolder->hit(mMe, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); - return btScalar(1); + return 1; } btVector3 hitNormalWorld; @@ -101,7 +94,7 @@ namespace MWPhysics // dot product of the motion vector against the collision contact normal btScalar dotCollision = mMotion.dot(hitNormalWorld); if (dotCollision <= mMinCollisionDot) - return btScalar(1); + return 1; return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); } diff --git a/apps/openmw/mwphysics/actorconvexcallback.hpp b/apps/openmw/mwphysics/actorconvexcallback.hpp index 4b9ab1a8a4..8442097a09 100644 --- a/apps/openmw/mwphysics/actorconvexcallback.hpp +++ b/apps/openmw/mwphysics/actorconvexcallback.hpp @@ -10,8 +10,15 @@ namespace MWPhysics class ActorConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ActorConvexCallback(const btCollisionObject* me, const btVector3& motion, btScalar minCollisionDot, - const btCollisionWorld* world); + explicit ActorConvexCallback(const btCollisionObject* me, const btVector3& motion, btScalar minCollisionDot, + const btCollisionWorld* world) + : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)) + , mMe(me) + , mMotion(motion) + , mMinCollisionDot(minCollisionDot) + , mWorld(world) + { + } btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) override; diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp index 30a42bc3b8..b63cd568a8 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp @@ -1,7 +1,6 @@ #include "closestnotmerayresultcallback.hpp" #include -#include #include @@ -9,19 +8,11 @@ namespace MWPhysics { - ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, - std::vector targets, const btVector3& from, const btVector3& to) - : btCollisionWorld::ClosestRayResultCallback(from, to) - , mMe(me) - , mTargets(std::move(targets)) - { - } - btScalar ClosestNotMeRayResultCallback::addSingleResult( btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { const auto* hitObject = rayResult.m_collisionObject; - if (hitObject == mMe) + if (std::find(mIgnoreList.begin(), mIgnoreList.end(), hitObject) != mIgnoreList.end()) return 1.f; if (hitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp index e6f5c45d36..660f24424d 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H #define OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H -#include +#include #include @@ -14,14 +14,19 @@ namespace MWPhysics class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { public: - ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, - const btVector3& from, const btVector3& to); + explicit ClosestNotMeRayResultCallback(std::span ignore, + std::span targets, const btVector3& from, const btVector3& to) + : btCollisionWorld::ClosestRayResultCallback(from, to) + , mIgnoreList(ignore) + , mTargets(targets) + { + } btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override; private: - const btCollisionObject* mMe; - const std::vector mTargets; + const std::span mIgnoreList; + const std::span mTargets; }; } diff --git a/apps/openmw/mwphysics/contacttestresultcallback.cpp b/apps/openmw/mwphysics/contacttestresultcallback.cpp index dae0a65af0..45d1127de4 100644 --- a/apps/openmw/mwphysics/contacttestresultcallback.cpp +++ b/apps/openmw/mwphysics/contacttestresultcallback.cpp @@ -8,13 +8,8 @@ namespace MWPhysics { - ContactTestResultCallback::ContactTestResultCallback(const btCollisionObject* testedAgainst) - : mTestedAgainst(testedAgainst) - { - } - btScalar ContactTestResultCallback::addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap, - int partId0, int index0, const btCollisionObjectWrapper* col1Wrap, int partId1, int index1) + int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* col1Wrap, int /*partId1*/, int /*index1*/) { const btCollisionObject* collisionObject = col0Wrap->m_collisionObject; if (collisionObject == mTestedAgainst) diff --git a/apps/openmw/mwphysics/contacttestresultcallback.hpp b/apps/openmw/mwphysics/contacttestresultcallback.hpp index ae900e0208..a9ba06368c 100644 --- a/apps/openmw/mwphysics/contacttestresultcallback.hpp +++ b/apps/openmw/mwphysics/contacttestresultcallback.hpp @@ -14,15 +14,19 @@ namespace MWPhysics { class ContactTestResultCallback : public btCollisionWorld::ContactResultCallback { - const btCollisionObject* mTestedAgainst; - public: - ContactTestResultCallback(const btCollisionObject* testedAgainst); + explicit ContactTestResultCallback(const btCollisionObject* testedAgainst) + : mTestedAgainst(testedAgainst) + { + } btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap, int partId0, int index0, const btCollisionObjectWrapper* col1Wrap, int partId1, int index1) override; std::vector mResult; + + private: + const btCollisionObject* mTestedAgainst; }; } diff --git a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp deleted file mode 100644 index 766ca79796..0000000000 --- a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "deepestnotmecontacttestresultcallback.hpp" - -#include - -#include - -#include "collisiontype.hpp" - -namespace MWPhysics -{ - - DeepestNotMeContactTestResultCallback::DeepestNotMeContactTestResultCallback( - const btCollisionObject* me, const std::vector& targets, const btVector3& origin) - : mMe(me) - , mTargets(targets) - , mOrigin(origin) - , mLeastDistSqr(std::numeric_limits::max()) - { - } - - btScalar DeepestNotMeContactTestResultCallback::addSingleResult(btManifoldPoint& cp, - const btCollisionObjectWrapper* col0Wrap, int partId0, int index0, const btCollisionObjectWrapper* col1Wrap, - int partId1, int index1) - { - const btCollisionObject* collisionObject = col1Wrap->m_collisionObject; - if (collisionObject != mMe) - { - if (collisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor - && !mTargets.empty()) - { - if ((std::find(mTargets.begin(), mTargets.end(), collisionObject) == mTargets.end())) - return 0.f; - } - - btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); - if (!mObject || distsqr < mLeastDistSqr) - { - mObject = collisionObject; - mLeastDistSqr = distsqr; - mContactPoint = cp.getPositionWorldOnA(); - mContactNormal = cp.m_normalWorldOnB; - } - } - - return 0.f; - } -} diff --git a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp deleted file mode 100644 index d22a79e643..0000000000 --- a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OPENMW_MWPHYSICS_DEEPESTNOTMECONTACTTESTRESULTCALLBACK_H -#define OPENMW_MWPHYSICS_DEEPESTNOTMECONTACTTESTRESULTCALLBACK_H - -#include - -#include - -class btCollisionObject; - -namespace MWPhysics -{ - class DeepestNotMeContactTestResultCallback : public btCollisionWorld::ContactResultCallback - { - const btCollisionObject* mMe; - const std::vector mTargets; - - // Store the real origin, since the shape's origin is its center - btVector3 mOrigin; - - public: - const btCollisionObject* mObject{ nullptr }; - btVector3 mContactPoint{ 0, 0, 0 }; - btVector3 mContactNormal{ 0, 0, 0 }; - btScalar mLeastDistSqr; - - DeepestNotMeContactTestResultCallback( - const btCollisionObject* me, const std::vector& targets, const btVector3& origin); - - btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap, int partId0, int index0, - const btCollisionObjectWrapper* col1Wrap, int partId1, int index1) override; - }; -} - -#endif diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index c0b5014b31..554cd7586a 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -15,6 +15,7 @@ #include "collisiontype.hpp" #include "constants.hpp" #include "contacttestwrapper.h" +#include "object.hpp" #include "physicssystem.hpp" #include "projectile.hpp" #include "projectileconvexcallback.hpp" @@ -31,53 +32,58 @@ namespace MWPhysics return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } - class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback + namespace { - public: - ContactCollectionCallback(const btCollisionObject* me, osg::Vec3f velocity) - : mMe(me) + class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { - m_collisionFilterGroup = me->getBroadphaseHandle()->m_collisionFilterGroup; - m_collisionFilterMask = me->getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile; - mVelocity = Misc::Convert::toBullet(velocity); - } - btScalar addSingleResult(btManifoldPoint& contact, const btCollisionObjectWrapper* colObj0Wrap, int partId0, - int index0, const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1) override - { - if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) - return 0.0; - // ignore overlap if we're moving in the same direction as it would push us out (don't change this to >=, - // that would break detection when not moving) - if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) - return 0.0; - auto delta = contact.m_normalWorldOnB * -contact.m_distance1; - mContactSum += delta; - mMaxX = std::max(std::abs(delta.x()), mMaxX); - mMaxY = std::max(std::abs(delta.y()), mMaxY); - mMaxZ = std::max(std::abs(delta.z()), mMaxZ); - if (contact.m_distance1 < mDistance) + public: + explicit ContactCollectionCallback(const btCollisionObject& me, const osg::Vec3f& velocity) + : mVelocity(Misc::Convert::toBullet(velocity)) { - mDistance = contact.m_distance1; - mNormal = contact.m_normalWorldOnB; - mDelta = delta; - return mDistance; + m_collisionFilterGroup = me.getBroadphaseHandle()->m_collisionFilterGroup; + m_collisionFilterMask = me.getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile; } - else + + btScalar addSingleResult(btManifoldPoint& contact, const btCollisionObjectWrapper* colObj0Wrap, + int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* colObj1Wrap, int /*partId1*/, + int /*index1*/) override { - return 0.0; + if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) + return 0.0; + // ignore overlap if we're moving in the same direction as it would push us out (don't change this to + // >=, that would break detection when not moving) + if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) + return 0.0; + auto delta = contact.m_normalWorldOnB * -contact.m_distance1; + mContactSum += delta; + mMaxX = std::max(std::abs(delta.x()), mMaxX); + mMaxY = std::max(std::abs(delta.y()), mMaxY); + mMaxZ = std::max(std::abs(delta.z()), mMaxZ); + if (contact.m_distance1 < mDistance) + { + mDistance = contact.m_distance1; + mNormal = contact.m_normalWorldOnB; + mDelta = delta; + return mDistance; + } + else + { + return 0.0; + } } - } - btScalar mMaxX = 0.0; - btScalar mMaxY = 0.0; - btScalar mMaxZ = 0.0; - btVector3 mContactSum{ 0.0, 0.0, 0.0 }; - btVector3 mNormal{ 0.0, 0.0, 0.0 }; // points towards "me" - btVector3 mDelta{ 0.0, 0.0, 0.0 }; // points towards "me" - btScalar mDistance = 0.0; // negative or zero - protected: - btVector3 mVelocity; - const btCollisionObject* mMe; - }; + + btScalar mMaxX = 0.0; + btScalar mMaxY = 0.0; + btScalar mMaxZ = 0.0; + btVector3 mContactSum{ 0.0, 0.0, 0.0 }; + btVector3 mNormal{ 0.0, 0.0, 0.0 }; // points towards "me" + btVector3 mDelta{ 0.0, 0.0, 0.0 }; // points towards "me" + btScalar mDistance = 0.0; // negative or zero + + protected: + btVector3 mVelocity; + }; + } osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight) @@ -172,15 +178,10 @@ namespace MWPhysics // Now that we have the effective movement vector, apply wind forces to it if (worldData.mIsInStorm && velocity.length() > 0) { - osg::Vec3f stormDirection = worldData.mStormDirection; - float angleDegrees = osg::RadiansToDegrees( - std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); - static const float fStromWalkMult = MWBase::Environment::get() - .getESMStore() - ->get() - .find("fStromWalkMult") - ->mValue.getFloat(); - velocity *= 1.f - (fStromWalkMult * (angleDegrees / 180.f)); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const float fStromWalkMult = store.get().find("fStromWalkMult")->mValue.getFloat(); + const float angleCos = worldData.mStormDirection * velocity / velocity.length(); + velocity *= 1.f + fStromWalkMult * angleCos; } Stepper stepper(collisionWorld, actor.mCollisionObject); @@ -243,11 +244,20 @@ namespace MWPhysics float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + actor.mHalfExtentsZ; osg::Vec3f oldPosition = newPosition; bool usedStepLogic = false; - if (hitHeight < Constants::sStepSizeUp && !isActor(tracer.mHitObject)) + if (!isActor(tracer.mHitObject)) { - // Try to step up onto it. - // NOTE: this modifies newPosition and velocity on its own if successful - usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); + if (hitHeight < Constants::sStepSizeUp) + { + // Try to step up onto it. + // NOTE: this modifies newPosition and velocity on its own if successful + usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); + } + auto* ptrHolder = static_cast(tracer.mHitObject->getUserPointer()); + if (Object* hitObject = dynamic_cast(ptrHolder)) + { + hitObject->addCollision( + actor.mIsPlayer ? ScriptedCollisionType_Player : ScriptedCollisionType_Actor); + } } if (usedStepLogic) { @@ -444,8 +454,10 @@ namespace MWPhysics if (btFrom == btTo) return; + assert(projectile.mProjectile != nullptr); + ProjectileConvexCallback resultCallback( - projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, projectile.mProjectile); + projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, *projectile.mProjectile); resultCallback.m_collisionFilterMask = CollisionType_AnyPhysical; resultCallback.m_collisionFilterGroup = CollisionType_Projectile; @@ -514,7 +526,7 @@ namespace MWPhysics newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); actor.mCollisionObject->setWorldTransform(newTransform); - ContactCollectionCallback callback{ actor.mCollisionObject, velocity }; + ContactCollectionCallback callback(*actor.mCollisionObject, velocity); ContactTestWrapper::contactTest( const_cast(collisionWorld), actor.mCollisionObject, callback); return callback; diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 653380decf..aafefe7019 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -16,8 +16,8 @@ #include "components/debug/debuglog.hpp" #include "components/misc/convert.hpp" -#include "components/settings/settings.hpp" #include +#include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -275,10 +275,13 @@ namespace if (mAdvanceSimulation) { MWWorld::Ptr standingOn; - auto* ptrHolder - = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); - if (ptrHolder) - standingOn = ptrHolder->getPtr(); + if (frameData.mStandingOn != nullptr) + { + auto* const ptrHolder + = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); + if (ptrHolder != nullptr) + standingOn = ptrHolder->getPtr(); + } actor->setStandingOnPtr(standingOn); // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the // change @@ -314,7 +317,7 @@ namespace MWPhysics LockingPolicy detectLockingPolicy() { - if (Settings::Manager::getInt("async num threads", "Physics") < 1) + if (Settings::physics().mAsyncNumThreads < 1) return LockingPolicy::NoLocks; if (getMaxBulletSupportedThreads() > 1) return LockingPolicy::AllowSharedLocks; @@ -331,8 +334,8 @@ namespace MWPhysics case LockingPolicy::ExclusiveLocksOnly: return 1; case LockingPolicy::AllowSharedLocks: - return std::clamp( - Settings::Manager::getInt("async num threads", "Physics"), 0, getMaxBulletSupportedThreads()); + return static_cast(std::clamp( + Settings::physics().mAsyncNumThreads, 0, static_cast(getMaxBulletSupportedThreads()))); } throw std::runtime_error("Unsupported LockingPolicy: " @@ -407,7 +410,7 @@ namespace MWPhysics , mNumThreads(getNumThreads(mLockingPolicy)) , mNumJobs(0) , mRemainingSteps(0) - , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) + , mLOSCacheExpiry(Settings::physics().mLineofsightKeepInactiveCache) , mAdvanceSimulation(false) , mNextJob(0) , mNextLOS(0) @@ -650,15 +653,15 @@ namespace MWPhysics void PhysicsTaskScheduler::addCollisionObject( btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { - mCollisionObjects.insert(collisionObject); MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); + mCollisionObjects.insert(collisionObject); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { - mCollisionObjects.erase(collisionObject); MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); + mCollisionObjects.erase(collisionObject); mCollisionWorld->removeCollisionObject(collisionObject); } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 57f3711096..579211b489 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include #include #include diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index 6e0e2cdc7f..5936083f1d 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -23,6 +23,7 @@ namespace MWPhysics , mPosition(ptr.getRefData().getPosition().asVec3()) , mRotation(rotation) , mTaskScheduler(scheduler) + , mCollidedWith(ScriptedCollisionType_None) { mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); @@ -110,6 +111,9 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; + if (!mPtr.getRefData().getBaseNode()) + return false; + assert(mShapeInstance->mCollisionShape->isCompound()); btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); @@ -137,6 +141,7 @@ namespace MWPhysics osg::NodePath& nodePath = nodePathFound->second; osg::Matrixf matrix = osg::computeLocalToWorld(nodePath); + btVector3 scale = Misc::Convert::toBullet(matrix.getScale()); matrix.orthoNormalize(matrix); btTransform transform; @@ -145,8 +150,15 @@ namespace MWPhysics for (int j = 0; j < 3; ++j) transform.getBasis()[i][j] = matrix(j, i); // NB column/row major difference - // Note: we can not apply scaling here for now since we treat scaled shapes - // as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now + btCollisionShape* childShape = compound->getChildShape(shapeIndex); + btVector3 newScale = compound->getLocalScaling() * scale; + + if (childShape->getLocalScaling() != newScale) + { + childShape->setLocalScaling(newScale); + result = true; + } + if (!(transform == compound->getChildTransform(shapeIndex))) { compound->updateChildTransform(shapeIndex, transform); @@ -155,4 +167,20 @@ namespace MWPhysics } return result; } + + bool Object::collidedWith(ScriptedCollisionType type) const + { + return mCollidedWith & type; + } + + void Object::addCollision(ScriptedCollisionType type) + { + std::unique_lock lock(mPositionMutex); + mCollidedWith |= type; + } + + void Object::resetCollisions() + { + mCollidedWith = ScriptedCollisionType_None; + } } diff --git a/apps/openmw/mwphysics/object.hpp b/apps/openmw/mwphysics/object.hpp index 875f12d879..15b7c46926 100644 --- a/apps/openmw/mwphysics/object.hpp +++ b/apps/openmw/mwphysics/object.hpp @@ -18,6 +18,14 @@ namespace MWPhysics { class PhysicsTaskScheduler; + enum ScriptedCollisionType : char + { + ScriptedCollisionType_None = 0, + ScriptedCollisionType_Actor = 1, + // Note that this isn't 3, colliding with a player doesn't count as colliding with an actor + ScriptedCollisionType_Player = 2 + }; + class Object final : public PtrHolder { public: @@ -38,6 +46,9 @@ namespace MWPhysics /// @brief update object shape /// @return true if shape changed bool animateCollisionShapes(); + bool collidedWith(ScriptedCollisionType type) const; + void addCollision(ScriptedCollisionType type); + void resetCollisions(); private: osg::ref_ptr mShapeInstance; @@ -50,6 +61,7 @@ namespace MWPhysics bool mTransformUpdatePending = false; mutable std::mutex mPositionMutex; PhysicsTaskScheduler* mTaskScheduler; + char mCollidedWith; }; } diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5f13fef571..e236db4633 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -49,7 +49,6 @@ #include "closestnotmerayresultcallback.hpp" #include "contacttestresultcallback.hpp" -#include "deepestnotmecontacttestresultcallback.hpp" #include "hasspherecollisioncallback.hpp" #include "heightfield.hpp" #include "movementsolver.hpp" @@ -69,7 +68,7 @@ namespace // Advance acrobatics and set flag for GetPCJumping if (isPlayer) { - ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); + ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Jump); MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); } @@ -94,8 +93,9 @@ namespace namespace MWPhysics { PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode) - : mShapeManager(std::make_unique( - resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager())) + : mShapeManager( + std::make_unique(resourceSystem->getVFS(), resourceSystem->getSceneManager(), + resourceSystem->getNifFileManager(), Settings::cells().mCacheExpiryDelay)) , mResourceSystem(resourceSystem) , mDebugDrawEnabled(false) , mTimeAccum(0.0f) @@ -191,95 +191,9 @@ namespace MWPhysics return true; } - std::pair PhysicsSystem::getHitContact(const MWWorld::ConstPtr& actor, - const osg::Vec3f& origin, const osg::Quat& orient, float queryDistance, std::vector& targets) - { - // First of all, try to hit where you aim to - int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - RayCastingResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, - targets, hitmask, CollisionType_Actor); - - if (result.mHit) - { - reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); - return std::make_pair(result.mHitObject, result.mHitPos); - } - - // Use cone shape as fallback - const MWWorld::Store& store - = MWBase::Environment::get().getESMStore()->get(); - - btConeShape shape(osg::DegreesToRadians(store.find("fCombatAngleXY")->mValue.getFloat() / 2.0f), queryDistance); - shape.setLocalScaling(btVector3( - 1, 1, osg::DegreesToRadians(store.find("fCombatAngleZ")->mValue.getFloat() / 2.0f) / shape.getRadius())); - - // The shape origin is its center, so we have to move it forward by half the length. The - // real origin will be provided to getFilteredContact to find the closest. - osg::Vec3f center = origin + (orient * osg::Vec3f(0.0f, queryDistance * 0.5f, 0.0f)); - - btCollisionObject object; - object.setCollisionShape(&shape); - object.setWorldTransform(btTransform(Misc::Convert::toBullet(orient), Misc::Convert::toBullet(center))); - - const btCollisionObject* me = nullptr; - std::vector targetCollisionObjects; - - const Actor* physactor = getActor(actor); - if (physactor) - me = physactor->getCollisionObject(); - - if (!targets.empty()) - { - for (MWWorld::Ptr& target : targets) - { - const Actor* targetActor = getActor(target); - if (targetActor) - targetCollisionObjects.push_back(targetActor->getCollisionObject()); - } - } - - DeepestNotMeContactTestResultCallback resultCallback( - me, targetCollisionObjects, Misc::Convert::toBullet(origin)); - resultCallback.m_collisionFilterGroup = CollisionType_Actor; - resultCallback.m_collisionFilterMask - = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - mTaskScheduler->contactTest(&object, resultCallback); - - if (resultCallback.mObject) - { - PtrHolder* holder = static_cast(resultCallback.mObject->getUserPointer()); - if (holder) - { - reportCollision(resultCallback.mContactPoint, resultCallback.mContactNormal); - return std::make_pair(holder->getPtr(), Misc::Convert::toOsg(resultCallback.mContactPoint)); - } - } - return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); - } - - float PhysicsSystem::getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const - { - btCollisionObject* targetCollisionObj = nullptr; - const Actor* actor = getActor(target); - if (actor) - targetCollisionObj = actor->getCollisionObject(); - if (!targetCollisionObj) - return 0.f; - - btTransform rayFrom; - rayFrom.setIdentity(); - rayFrom.setOrigin(Misc::Convert::toBullet(point)); - - auto hitpoint = mTaskScheduler->getHitPoint(rayFrom, targetCollisionObj); - if (hitpoint) - return (point - Misc::Convert::toOsg(*hitpoint)).length(); - - // didn't hit the target. this could happen if point is already inside the collision box - return 0.f; - } - RayCastingResult PhysicsSystem::castRay(const osg::Vec3f& from, const osg::Vec3f& to, - const MWWorld::ConstPtr& ignore, const std::vector& targets, int mask, int group) const + const std::vector& ignore, const std::vector& targets, int mask, + int group) const { if (from == to) { @@ -290,19 +204,22 @@ namespace MWPhysics btVector3 btFrom = Misc::Convert::toBullet(from); btVector3 btTo = Misc::Convert::toBullet(to); - const btCollisionObject* me = nullptr; + std::vector ignoreList; std::vector targetCollisionObjects; - if (!ignore.isEmpty()) + for (const auto& ptr : ignore) { - const Actor* actor = getActor(ignore); - if (actor) - me = actor->getCollisionObject(); - else + if (!ptr.isEmpty()) { - const Object* object = getObject(ignore); - if (object) - me = object->getCollisionObject(); + const Actor* actor = getActor(ptr); + if (actor) + ignoreList.push_back(actor->getCollisionObject()); + else + { + const Object* object = getObject(ptr); + if (object) + ignoreList.push_back(object->getCollisionObject()); + } } } @@ -316,7 +233,7 @@ namespace MWPhysics } } - ClosestNotMeRayResultCallback resultCallback(me, targetCollisionObjects, btFrom, btTo); + ClosestNotMeRayResultCallback resultCallback(ignoreList, targetCollisionObjects, btFrom, btTo); resultCallback.m_collisionFilterGroup = group; resultCallback.m_collisionFilterMask = mask; @@ -490,14 +407,14 @@ namespace MWPhysics } void PhysicsSystem::addObject( - const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType) + const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh, osg::Quat rotation, int collisionType) { if (ptr.mRef->mData.mPhysicsPostponed) return; - std::string animationMesh = mesh; - if (ptr.getClass().useAnim()) - animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + const VFS::Path::Normalized animationMesh = ptr.getClass().useAnim() + ? Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()) + : VFS::Path::Normalized(mesh); osg::ref_ptr shapeInstance = mShapeManager->getInstance(animationMesh); if (!shapeInstance || !shapeInstance->mCollisionShape) return; @@ -643,9 +560,10 @@ namespace MWPhysics } } - void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, const std::string& mesh) + void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh) { - std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + const VFS::Path::Normalized animationMesh + = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); osg::ref_ptr shape = mShapeManager->getShape(animationMesh); // Try to get shape from basic model as fallback for creatures @@ -671,7 +589,7 @@ namespace MWPhysics } int PhysicsSystem::addProjectile( - const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) + const MWWorld::Ptr& caster, const osg::Vec3f& position, VFS::Path::NormalizedView mesh, bool computeRadius) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); @@ -759,12 +677,13 @@ namespace MWPhysics // Slow fall reduces fall speed by a factor of (effect magnitude / 200) const float slowFall = 1.f - std::clamp(effects.getOrDefault(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f); - const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); + const bool isPlayer = ptr == world->getPlayerConstPtr(); + const bool godmode = isPlayer && world->getGodModeState(); const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Paralyze).getModifier() > 0); simulations.emplace_back(ActorSimulation{ - physicActor, ActorFrameData{ *physicActor, inert, waterCollision, slowFall, waterlevel } }); + physicActor, ActorFrameData{ *physicActor, inert, waterCollision, slowFall, waterlevel, isPlayer } }); // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. if (willSimulate) @@ -794,6 +713,8 @@ namespace MWPhysics changed = false; } } + for (auto& [_, object] : mObjects) + object->resetCollisions(); #ifndef BT_NO_PROFILE CProfileManager::Reset(); @@ -868,10 +789,12 @@ namespace MWPhysics } } - bool PhysicsSystem::isActorCollidingWith(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const + bool PhysicsSystem::isObjectCollidingWith(const MWWorld::ConstPtr& object, ScriptedCollisionType type) const { - std::vector collisions = getCollisions(object, CollisionType_World, CollisionType_Actor); - return (std::find(collisions.begin(), collisions.end(), actor) != collisions.end()); + auto found = mObjects.find(object.mRef); + if (found != mObjects.end()) + return found->second->collidedWith(type); + return false; } void PhysicsSystem::getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const @@ -976,7 +899,8 @@ namespace MWPhysics mDebugDrawer->addCollision(position, normal); } - ActorFrameData::ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel) + ActorFrameData::ActorFrameData( + Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel, bool isPlayer) : mPosition() , mStandingOn(nullptr) , mIsOnGround(actor.getOnGround()) @@ -1003,6 +927,7 @@ namespace MWPhysics , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) , mWaterCollision(waterCollision) , mSkipCollisionDetection(!actor.getCollisionMode()) + , mIsPlayer(isPlayer) { } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index e4c1b63776..546d72676e 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -1,12 +1,12 @@ #ifndef OPENMW_MWPHYSICS_PHYSICSSYSTEM_H #define OPENMW_MWPHYSICS_PHYSICSSYSTEM_H +#include #include #include #include #include #include -#include #include #include #include @@ -16,7 +16,7 @@ #include #include -#include +#include #include "../mwworld/ptr.hpp" @@ -56,6 +56,7 @@ namespace MWPhysics class Actor; class PhysicsTaskScheduler; class Projectile; + enum ScriptedCollisionType : char; using ActorMap = std::unordered_map>; @@ -79,7 +80,7 @@ namespace MWPhysics struct ActorFrameData { - ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel); + ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel, bool isPlayer); osg::Vec3f mPosition; osg::Vec3f mInertia; const btCollisionObject* mStandingOn; @@ -102,6 +103,7 @@ namespace MWPhysics const bool mIsAquatic; const bool mWaterCollision; const bool mSkipCollisionDetection; + const bool mIsPlayer; }; struct ProjectileFrameData @@ -159,12 +161,12 @@ namespace MWPhysics void setWaterHeight(float height); void disableWater(); - void addObject(const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, + void addObject(const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh, osg::Quat rotation, int collisionType = CollisionType_World); - void addActor(const MWWorld::Ptr& ptr, const std::string& mesh); + void addActor(const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh); int addProjectile( - const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); + const MWWorld::Ptr& caster, const osg::Vec3f& position, VFS::Path::NormalizedView mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); void removeProjectile(const int projectileId); @@ -207,21 +209,11 @@ namespace MWPhysics const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const; osg::Vec3f traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, float maxHeight); - std::pair getHitContact(const MWWorld::ConstPtr& actor, const osg::Vec3f& origin, - const osg::Quat& orientation, float queryDistance, std::vector& targets); - - /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the - /// target vector hits the collision shape and then calculates distance from the intersection point. - /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be - /// successful. \note Only Actor targets are supported at the moment. - float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const override; - - /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all - /// other actors. + /// @param ignore Optional, a list of Ptr to ignore in the list of results. targets are actors to filter for, + /// ignoring all other actors. RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, - const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), - const std::vector& targets = std::vector(), int mask = CollisionType_Default, - int group = 0xff) const override; + const std::vector& ignore = {}, const std::vector& targets = {}, + int mask = CollisionType_Default, int group = 0xff) const override; using RayCastingInterface::castRay; RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, @@ -267,9 +259,8 @@ namespace MWPhysics /// Get the handle of all actors standing on \a object in this frame. void getActorsStandingOn(const MWWorld::ConstPtr& object, std::vector& out) const; - /// Return true if \a actor has collided with \a object in this frame. - /// This will detect running into objects, but will not detect climbing stairs, stepping up a small object, etc. - bool isActorCollidingWith(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const; + /// Return true if an object of the given type has collided with this object + bool isObjectCollidingWith(const MWWorld::ConstPtr& object, ScriptedCollisionType type) const; /// Get the handle of all actors colliding with \a object in this frame. void getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const; diff --git a/apps/openmw/mwphysics/projectileconvexcallback.cpp b/apps/openmw/mwphysics/projectileconvexcallback.cpp index d7e80b4698..913a3edb0c 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.cpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.cpp @@ -6,16 +6,6 @@ namespace MWPhysics { - ProjectileConvexCallback::ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, - const btVector3& from, const btVector3& to, Projectile* proj) - : btCollisionWorld::ClosestConvexResultCallback(from, to) - , mCaster(caster) - , mMe(me) - , mProjectile(proj) - { - assert(mProjectile); - } - btScalar ProjectileConvexCallback::addSingleResult( btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) { @@ -33,25 +23,25 @@ namespace MWPhysics { case CollisionType_Actor: { - if (!mProjectile->isValidTarget(hitObject)) + if (!mProjectile.isValidTarget(hitObject)) return 1.f; break; } case CollisionType_Projectile: { auto* target = static_cast(hitObject->getUserPointer()); - if (!mProjectile->isValidTarget(target->getCasterCollisionObject())) + if (!mProjectile.isValidTarget(target->getCasterCollisionObject())) return 1.f; target->hit(mMe, m_hitPointWorld, m_hitNormalWorld); break; } case CollisionType_Water: { - mProjectile->setHitWater(); + mProjectile.setHitWater(); break; } } - mProjectile->hit(hitObject, m_hitPointWorld, m_hitNormalWorld); + mProjectile.hit(hitObject, m_hitPointWorld, m_hitNormalWorld); return result.m_hitFraction; } diff --git a/apps/openmw/mwphysics/projectileconvexcallback.hpp b/apps/openmw/mwphysics/projectileconvexcallback.hpp index 3cd304bab0..d75ace22af 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.hpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.hpp @@ -12,15 +12,21 @@ namespace MWPhysics class ProjectileConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, const btVector3& from, - const btVector3& to, Projectile* proj); + explicit ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, + const btVector3& from, const btVector3& to, Projectile& projectile) + : btCollisionWorld::ClosestConvexResultCallback(from, to) + , mCaster(caster) + , mMe(me) + , mProjectile(projectile) + { + } btScalar addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) override; private: const btCollisionObject* mCaster; const btCollisionObject* mMe; - Projectile* mProjectile; + Projectile& mProjectile; }; } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index 4a56e9bf33..78b6ab4678 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -23,22 +23,15 @@ namespace MWPhysics public: virtual ~RayCastingInterface() = default; - /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the - /// target vector hits the collision shape and then calculates distance from the intersection point. - /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be - /// successful. \note Only Actor targets are supported at the moment. - virtual float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const = 0; - - /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all - /// other actors. + /// @param ignore Optional, a list of Ptr to ignore in the list of results. targets are actors to filter for, + /// ignoring all other actors. virtual RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, - const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), - const std::vector& targets = std::vector(), int mask = CollisionType_Default, - int group = 0xff) const = 0; + const std::vector& ignore = {}, const std::vector& targets = {}, + int mask = CollisionType_Default, int group = 0xff) const = 0; RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask) const { - return castRay(from, to, MWWorld::ConstPtr(), std::vector(), mask); + return castRay(from, to, {}, {}, mask); } virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 600ae6f0ed..9d68bf9bbd 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -35,6 +35,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/ptr.hpp" +#include "actorutil.hpp" #include "vismask.hpp" namespace MWRender @@ -66,7 +67,7 @@ namespace MWRender } PartHolderPtr ActorAnimation::attachMesh( - const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor) + VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor) { osg::Group* parent = getBoneByName(bonename); if (!parent) @@ -79,14 +80,14 @@ namespace MWRender if (found == nodeMap.end()) return {}; - if (enchantedGlow) + if (glowColor != nullptr) mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); return std::make_unique(instance); } osg::ref_ptr ActorAnimation::attach( - const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight) + VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight) { osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); @@ -101,7 +102,7 @@ namespace MWRender templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); } return SceneUtil::attach( - templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); + std::move(templateNode), mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); } std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const @@ -130,12 +131,11 @@ namespace MWRender if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) return std::string(); if (!bodypart->mModel.empty()) - return Misc::ResourceHelpers::correctMeshPath( - bodypart->mModel, MWBase::Environment::get().getResourceSystem()->getVFS()); + return Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bodypart->mModel)).value(); } } } - return shield.getClass().getModel(shield); + return shield.getClass().getCorrectedModel(shield); } std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const @@ -145,8 +145,7 @@ namespace MWRender if (mesh.empty()) return mesh; - std::string holsteredName = mesh; - holsteredName = holsteredName.replace(holsteredName.size() - 4, 4, "_sh.nif"); + const VFS::Path::Normalized holsteredName(addSuffixBeforeExtension(mesh, "_sh")); if (mResourceSystem->getVFS()->exists(holsteredName)) { osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); @@ -217,21 +216,20 @@ namespace MWRender return; } - std::string mesh = getSheathedShieldMesh(*shield); + const VFS::Path::Normalized mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; - std::string_view boneName = "Bip01 AttachShield"; - osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); - std::string holsteredName = mesh; - holsteredName = holsteredName.replace(holsteredName.size() - 4, 4, "_sh.nif"); - bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + constexpr std::string_view boneName = "Bip01 AttachShield"; + const bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + const osg::Vec4f glowColor = isEnchanted ? shield->getClass().getEnchantmentColor(*shield) : osg::Vec4f(); + const VFS::Path::Normalized holsteredName = addSuffixBeforeExtension(mesh, "_sh"); // If we have no dedicated sheath model, use basic shield model as fallback. if (!mResourceSystem->getVFS()->exists(holsteredName)) - mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); + mHolsteredShield = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr); else - mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); + mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted ? &glowColor : nullptr); if (!mHolsteredShield) return; @@ -315,6 +313,7 @@ namespace MWRender if (node == nullptr) return; + // This is used to avoid playing animations intended for equipped weapons on holstered weapons. SceneUtil::ForceControllerSourcesVisitor removeVisitor(std::make_shared()); node->accept(removeVisitor); } @@ -336,28 +335,29 @@ namespace MWRender // Since throwing weapons stack themselves, do not show such weapon itself int type = weapon->get()->mBase->mData.mType; - if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) + auto weaponClass = MWMechanics::getWeaponType(type)->mWeaponClass; + if (weaponClass == ESM::WeaponType::Thrown) showHolsteredWeapons = false; - std::string mesh = weapon->getClass().getModel(*weapon); - std::string scabbardName = mesh; - - std::string_view boneName = getHolsteredWeaponBoneName(*weapon); - if (mesh.empty() || boneName.empty()) + const VFS::Path::Normalized mesh = weapon->getClass().getCorrectedModel(*weapon); + if (mesh.empty()) return; - // If the scabbard is not found, use a weapon mesh as fallback. - // Note: it is unclear how to handle time for controllers attached to bodyparts, so disable them for now. - // We use the similar approach for other bodyparts. - scabbardName = scabbardName.replace(scabbardName.size() - 4, 4, "_sh.nif"); - bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); + const std::string_view boneName = getHolsteredWeaponBoneName(*weapon); + if (boneName.empty()) + return; + + // If the scabbard is not found, use the weapon mesh as fallback. + const VFS::Path::Normalized scabbardName = addSuffixBeforeExtension(mesh, "_sh"); + const bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); if (!mResourceSystem->getVFS()->exists(scabbardName)) { if (showHolsteredWeapons) { - osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); - mScabbard = attachMesh(mesh, boneName, isEnchanted, &glowColor); - if (mScabbard) + const osg::Vec4f glowColor + = isEnchanted ? weapon->getClass().getEnchantmentColor(*weapon) : osg::Vec4f(); + mScabbard = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr); + if (mScabbard && weaponClass == ESM::WeaponType::Ranged) resetControllers(mScabbard->getNode()); } @@ -365,7 +365,7 @@ namespace MWRender } mScabbard = attachMesh(scabbardName, boneName); - if (mScabbard) + if (mScabbard && weaponClass == ESM::WeaponType::Ranged) resetControllers(mScabbard->getNode()); osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); @@ -387,7 +387,9 @@ namespace MWRender { osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, weaponNode); - resetControllers(fallbackNode); + + if (weaponClass == ESM::WeaponType::Ranged) + resetControllers(fallbackNode); } if (isEnchanted) @@ -411,7 +413,7 @@ namespace MWRender if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; - std::string mesh = weapon->getClass().getModel(*weapon); + std::string_view mesh = weapon->getClass().getModel(*weapon); std::string_view boneName = getHolsteredWeaponBoneName(*weapon); if (mesh.empty() || boneName.empty()) return; @@ -428,7 +430,7 @@ namespace MWRender const auto& weaponType = MWMechanics::getWeaponType(type); if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) { - ammoCount = ammo->getRefData().getCount(); + ammoCount = ammo->getCellRef().getCount(); osg::Group* throwingWeaponNode = getBoneByName(weaponType->mAttachBone); if (throwingWeaponNode && throwingWeaponNode->getNumChildren()) ammoCount--; @@ -441,7 +443,7 @@ namespace MWRender if (ammo == inv.end()) return; - ammoCount = ammo->getRefData().getCount(); + ammoCount = ammo->getCellRef().getCount(); bool arrowAttached = isArrowAttached(); if (arrowAttached) ammoCount--; @@ -468,7 +470,7 @@ namespace MWRender // Add new ones osg::Vec4f glowColor = ammo->getClass().getEnchantmentColor(*ammo); - std::string model = ammo->getClass().getModel(*ammo); + const VFS::Path::Normalized model(ammo->getClass().getCorrectedModel(*ammo)); for (unsigned int i = 0; i < ammoCount; ++i) { osg::ref_ptr arrowNode = ammoNode->getChild(i)->asGroup(); @@ -516,7 +518,7 @@ namespace MWRender ItemLightMap::iterator iter = mItemLights.find(item); if (iter != mItemLights.end()) { - if (!item.getRefData().getCount()) + if (!item.getCellRef().getCount()) { removeHiddenItemLight(item); } diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index b6586d4eab..0182df9370 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -5,6 +5,8 @@ #include +#include + #include "../mwworld/containerstore.hpp" #include "animation.hpp" @@ -45,21 +47,18 @@ namespace MWRender protected: osg::Group* getBoneByName(std::string_view boneName) const; - virtual void updateHolsteredWeapon(bool showHolsteredWeapons); - virtual void updateHolsteredShield(bool showCarriedLeft); - virtual void updateQuiver(); + void updateHolsteredWeapon(bool showHolsteredWeapons); + void updateHolsteredShield(bool showCarriedLeft); + void updateQuiver(); std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; - virtual std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); - virtual PartHolderPtr attachMesh( - const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor); - virtual PartHolderPtr attachMesh(const std::string& model, std::string_view bonename) - { - osg::Vec4f stubColor = osg::Vec4f(0, 0, 0, 0); - return attachMesh(model, bonename, false, &stubColor); - } + std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); + + PartHolderPtr attachMesh( + VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor = nullptr); + osg::ref_ptr attach( - const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight); + VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; diff --git a/apps/openmw/mwrender/actorspaths.cpp b/apps/openmw/mwrender/actorspaths.cpp index 68cf73fb52..2a36e99f79 100644 --- a/apps/openmw/mwrender/actorspaths.cpp +++ b/apps/openmw/mwrender/actorspaths.cpp @@ -1,12 +1,17 @@ #include "actorspaths.hpp" + #include "vismask.hpp" #include #include #include #include +#include +#include +#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -15,9 +20,32 @@ namespace MWRender { + namespace + { + osg::ref_ptr makeGroupStateSet() + { + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setAttribute(material); + return stateSet; + } + + osg::ref_ptr makeDebugDrawStateSet() + { + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setAttributeAndModes(new osg::LineWidth()); + + return stateSet; + } + } + ActorsPaths::ActorsPaths(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) + , mGroupStateSet(makeGroupStateSet()) + , mDebugDrawStateSet(makeDebugDrawStateSet()) { } @@ -48,14 +76,15 @@ namespace MWRender if (group != mGroups.end()) mRootNode->removeChild(group->second.mNode); - auto newGroup = SceneUtil::createAgentPathGroup(path, agentBounds, start, end, settings.mRecast); - if (newGroup) - { - MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(newGroup, "debug"); - newGroup->setNodeMask(Mask_Debug); - mRootNode->addChild(newGroup); - mGroups[actor.mRef] = Group{ actor.mCell, std::move(newGroup) }; - } + osg::ref_ptr newGroup + = SceneUtil::createAgentPathGroup(path, agentBounds, start, end, settings.mRecast, mDebugDrawStateSet); + newGroup->setNodeMask(Mask_Debug); + newGroup->setStateSet(mGroupStateSet); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(newGroup, "debug"); + + mRootNode->addChild(newGroup); + mGroups.insert_or_assign(group, actor.mRef, Group{ actor.mCell, std::move(newGroup) }); } void ActorsPaths::remove(const MWWorld::ConstPtr& actor) diff --git a/apps/openmw/mwrender/actorspaths.hpp b/apps/openmw/mwrender/actorspaths.hpp index d18197b974..61f246c777 100644 --- a/apps/openmw/mwrender/actorspaths.hpp +++ b/apps/openmw/mwrender/actorspaths.hpp @@ -1,17 +1,17 @@ #ifndef OPENMW_MWRENDER_AGENTSPATHS_H #define OPENMW_MWRENDER_AGENTSPATHS_H -#include +#include "apps/openmw/mwworld/ptr.hpp" #include #include #include -#include namespace osg { class Group; + class StateSet; } namespace DetourNavigator @@ -56,6 +56,8 @@ namespace MWRender osg::ref_ptr mRootNode; Groups mGroups; bool mEnabled; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; }; } diff --git a/apps/openmw/mwrender/actorutil.cpp b/apps/openmw/mwrender/actorutil.cpp new file mode 100644 index 0000000000..3cb8adb8aa --- /dev/null +++ b/apps/openmw/mwrender/actorutil.cpp @@ -0,0 +1,52 @@ +#include "actorutil.hpp" + +#include +#include + +namespace MWRender +{ + const std::string& getActorSkeleton(bool firstPerson, bool isFemale, bool isBeast, bool isWerewolf) + { + if (!firstPerson) + { + if (isWerewolf) + return Settings::models().mWolfskin.get().value(); + else if (isBeast) + return Settings::models().mBaseanimkna.get().value(); + else if (isFemale) + return Settings::models().mBaseanimfemale.get().value(); + else + return Settings::models().mBaseanim.get().value(); + } + else + { + if (isWerewolf) + return Settings::models().mWolfskin1st.get().value(); + else if (isBeast) + return Settings::models().mBaseanimkna1st.get().value(); + else if (isFemale) + return Settings::models().mBaseanimfemale1st.get().value(); + else + return Settings::models().mXbaseanim1st.get().value(); + } + } + + bool isDefaultActorSkeleton(std::string_view model) + { + return VFS::Path::pathEqual(Settings::models().mBaseanimkna.get(), model) + || VFS::Path::pathEqual(Settings::models().mBaseanimfemale.get(), model) + || VFS::Path::pathEqual(Settings::models().mBaseanim.get(), model); + } + + std::string addSuffixBeforeExtension(const std::string& filename, const std::string& suffix) + { + size_t dotPos = filename.rfind('.'); + + // No extension found; return the original filename with suffix appended + if (dotPos == std::string::npos) + return filename + suffix; + + // Insert the suffix before the dot (extension) and return the new filename + return filename.substr(0, dotPos) + suffix + filename.substr(dotPos); + } +} diff --git a/apps/openmw/mwrender/actorutil.hpp b/apps/openmw/mwrender/actorutil.hpp new file mode 100644 index 0000000000..6a5ab12dea --- /dev/null +++ b/apps/openmw/mwrender/actorutil.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_APPS_OPENMW_MWRENDER_ACTORUTIL_H +#define OPENMW_APPS_OPENMW_MWRENDER_ACTORUTIL_H + +#include +#include + +namespace MWRender +{ + const std::string& getActorSkeleton(bool firstPerson, bool female, bool beast, bool werewolf); + bool isDefaultActorSkeleton(std::string_view model); + std::string addSuffixBeforeExtension(const std::string& filename, const std::string& suffix); +} + +#endif diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 74ec8e6d78..2b5092087a 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -14,8 +13,12 @@ #include #include +#include +#include + #include +#include #include #include @@ -25,16 +28,17 @@ #include #include #include + #include #include #include -#include - -#include #include +#include +#include -#include +#include +#include #include #include #include @@ -46,6 +50,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" @@ -53,7 +58,9 @@ #include "../mwworld/esmstore.hpp" #include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority +#include "../mwmechanics/weapontype.hpp" +#include "actorutil.hpp" #include "rotatecontroller.hpp" #include "util.hpp" #include "vismask.hpp" @@ -300,11 +307,10 @@ namespace RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) - , mEffectId(-1) { } - RemoveCallbackVisitor(int effectId) + RemoveCallbackVisitor(std::string_view effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) @@ -323,7 +329,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - bool toRemove = mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId; + bool toRemove = mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId; if (toRemove) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else @@ -337,7 +343,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor @@ -347,11 +353,10 @@ namespace FindVfxCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mEffectId(-1) { } - FindVfxCallbacksVisitor(int effectId) + FindVfxCallbacksVisitor(std::string_view effectId) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(effectId) { @@ -367,7 +372,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId) + if (mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId) { mCallbacks.push_back(vfxCallback); } @@ -381,20 +386,77 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; - osg::ref_ptr getVFXLightModelInstance() + namespace { - static osg::ref_ptr lightModel = nullptr; - - if (!lightModel) + osg::ref_ptr makeVFXLightModelInstance() { - lightModel = new osg::LightModel; + osg::ref_ptr lightModel = new osg::LightModel; lightModel->setAmbientIntensity({ 1, 1, 1, 1 }); + return lightModel; } - return lightModel; + const osg::ref_ptr& getVFXLightModelInstance() + { + static const osg::ref_ptr lightModel = makeVFXLightModelInstance(); + return lightModel; + } + } + + void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot) + { + // Attempt to cast node to an osgAnimation::Bone + if (!isRoot && dynamic_cast(parent)) + { + // Wrapping in a custom callback object allows for nested callback chaining, otherwise it has link to self + // issues we need to share the base BoneAnimBlendController as that contains blending information and is + // guaranteed to update before + osgAnimation::Bone* bone = static_cast(parent); + osg::ref_ptr cb = new MWRender::BoneAnimBlendControllerWrapper(controller, bone); + + // Ensure there is no other AnimBlendController - this can happen when using + // multiple animations with different roots, such as NPC animation + osg::Callback* updateCb = bone->getUpdateCallback(); + while (updateCb) + { + if (dynamic_cast(updateCb)) + { + osg::ref_ptr nextCb = updateCb->getNestedCallback(); + bone->removeUpdateCallback(updateCb); + updateCb = nextCb; + } + else + { + updateCb = updateCb->getNestedCallback(); + } + } + + // Find UpdateBone callback and bind to just after that (order is important) + // NOTE: if it doesn't have an UpdateBone callback, we shouldn't be doing blending! + updateCb = bone->getUpdateCallback(); + while (updateCb) + { + if (dynamic_cast(updateCb)) + { + // Override the immediate callback after the UpdateBone + osg::ref_ptr lastCb = updateCb->getNestedCallback(); + updateCb->setNestedCallback(cb); + if (lastCb) + cb->setNestedCallback(lastCb); + break; + } + + updateCb = updateCb->getNestedCallback(); + } + } + + // Traverse child bones if this is a group + osg::Group* group = parent->asGroup(); + if (group) + for (unsigned int i = 0; i < group->getNumChildren(); ++i) + assignBoneBlendCallbackRecursive(controller, group->getChild(i), false); } } @@ -446,9 +508,11 @@ namespace MWRender typedef std::map> ControllerMap; - ControllerMap mControllerMap[Animation::sNumBlendMasks]; + ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; + + osg::ref_ptr mAnimBlendRules; }; void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) @@ -529,6 +593,8 @@ namespace MWRender , mBodyPitchRadians(0.f) , mHasMagicEffects(false) , mAlpha(1.f) + , mPlayScriptedOnly(false) + , mRequiresBoneMap(false) { for (size_t i = 0; i < sNumBlendMasks; i++) mAnimationTimePtr[i] = std::make_shared(); @@ -592,46 +658,57 @@ namespace MWRender return mKeyframes->mTextKeys; } - void Animation::loadAllAnimationsInFolder(const std::string& model, const std::string& baseModel) + void Animation::loadAdditionalAnimations(VFS::Path::NormalizedView model, const std::string& baseModel) { - std::string animationPath = model; - if (animationPath.find("meshes") == 0) - { - animationPath.replace(0, 6, "animations"); - } - animationPath.replace(animationPath.size() - 3, 3, "/"); + constexpr VFS::Path::NormalizedView meshes("meshes/"); + if (!model.value().starts_with(meshes.value())) + return; - for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) + std::string path(model.value()); + + constexpr VFS::Path::NormalizedView animations("animations/"); + path.replace(0, meshes.value().size(), animations.value()); + + const std::string::size_type extensionStart = path.find_last_of(VFS::Path::extensionSeparator); + if (extensionStart == std::string::npos) + return; + + path.replace(extensionStart, path.size() - extensionStart, "/"); + + for (const VFS::Path::Normalized& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(path)) { if (Misc::getFileExtension(name) == "kf") + { addSingleAnimSource(name, baseModel); + } } } void Animation::addAnimSource(std::string_view model, const std::string& baseModel) { - std::string kfname = Misc::StringUtils::lowerCase(model); + VFS::Path::Normalized kfname(model); - if (kfname.ends_with(".nif")) - kfname.replace(kfname.size() - 4, 4, ".kf"); + if (Misc::getFileExtension(kfname) == "nif") + kfname.changeExtension("kf"); addSingleAnimSource(kfname, baseModel); if (Settings::game().mUseAdditionalAnimSources) - loadAllAnimationsInFolder(kfname, baseModel); + loadAdditionalAnimations(kfname, baseModel); } - void Animation::addSingleAnimSource(const std::string& kfname, const std::string& baseModel) + std::shared_ptr Animation::addSingleAnimSource( + const std::string& kfname, const std::string& baseModel) { if (!mResourceSystem->getVFS()->exists(kfname)) - return; + return nullptr; auto animsrc = std::make_shared(); - animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname); + animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(VFS::Path::toNormalized(kfname)); if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() || animsrc->mKeyframes->mKeyframeControllers.empty()) - return; + return nullptr; const NodeMap& nodeMap = getNodeMap(); const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers; @@ -659,7 +736,7 @@ namespace MWRender animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned)); } - mAnimSources.push_back(std::move(animsrc)); + mAnimSources.push_back(animsrc); for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups()) mSupportedAnimations.insert(group); @@ -691,6 +768,37 @@ namespace MWRender break; } } + + // Get the blending rules + if (Settings::game().mSmoothAnimTransitions) + { + // Note, even if the actual config is .json - we should send a .yaml path to AnimBlendRulesManager, the + // manager will check for .json if it will not find a specified .yaml file. + VFS::Path::Normalized blendConfigPath(kfname); + blendConfigPath.changeExtension("yaml"); + + // globalBlendConfigPath is only used with actors! Objects have no default blending. + constexpr VFS::Path::NormalizedView globalBlendConfigPath("animations/animation-config.yaml"); + + osg::ref_ptr blendRules; + if (mPtr.getClass().isActor()) + { + blendRules + = mResourceSystem->getAnimBlendRulesManager()->getRules(globalBlendConfigPath, blendConfigPath); + if (blendRules == nullptr) + Log(Debug::Warning) << "Unable to find animation blending rules: '" << blendConfigPath << "' or '" + << globalBlendConfigPath << "'"; + } + else + { + blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(blendConfigPath, blendConfigPath); + } + + // At this point blendRules will either be nullptr or an AnimBlendRules instance with > 0 rules inside. + animsrc->mAnimBlendRules = blendRules; + } + + return animsrc; } void Animation::clearAnimSources() @@ -713,6 +821,41 @@ namespace MWRender return mSupportedAnimations.find(anim) != mSupportedAnimations.end(); } + bool Animation::isLoopingAnimation(std::string_view group) const + { + // In Morrowind, a some animation groups are always considered looping, regardless + // of loop start/stop keys. + // To be match vanilla behavior we probably only need to check this list, but we don't + // want to prevent modded animations with custom group names from looping either. + static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", + "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", + "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", + "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", + "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", + "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", + "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; + static const std::vector shortGroups = MWMechanics::getAllWeaponTypeShortGroups(); + + if (getTextKeyTime(std::string(group) + ": loop start") >= 0) + return true; + + // Most looping animations have variants for each weapon type shortgroup. + // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. + // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" + // when the shortgroup is crossbow. + std::size_t suffixLength = 0; + for (std::string_view suffix : shortGroups) + { + if (suffix.length() > suffixLength && group.ends_with(suffix)) + { + suffixLength = suffix.length(); + } + } + group.remove_suffix(suffixLength); + + return loopingAnimations.count(group) > 0; + } + float Animation::getStartTime(const std::string& groupname) const { for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) @@ -756,21 +899,19 @@ namespace MWRender state.mLoopStopTime = key->first; } - if (mTextKeyListener) + try { - try - { + if (mTextKeyListener != nullptr) mTextKeyListener->handleTextKey(groupname, key, map); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); - } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); } } void Animation::play(std::string_view groupname, const AnimPriority& priority, int blendMask, bool autodisable, - float speedmult, std::string_view start, std::string_view stop, float startpoint, size_t loops, + float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback) { if (!mObjectRoot || mAnimSources.empty()) @@ -782,19 +923,23 @@ namespace MWRender return; } + AnimStateMap::iterator foundstateiter = mStates.find(groupname); + if (foundstateiter != mStates.end()) + { + foundstateiter->second.mPriority = priority; + } + AnimStateMap::iterator stateiter = mStates.begin(); while (stateiter != mStates.end()) { - if (stateiter->second.mPriority == priority) + if (stateiter->second.mPriority == priority && stateiter->first != groupname) mStates.erase(stateiter++); else ++stateiter; } - stateiter = mStates.find(groupname); - if (stateiter != mStates.end()) + if (foundstateiter != mStates.end()) { - stateiter->second.mPriority = priority; resetActiveGroups(); return; } @@ -814,6 +959,8 @@ namespace MWRender state.mPriority = priority; state.mBlendMask = blendMask; state.mAutoDisable = autodisable; + state.mGroupname = groupname; + state.mStartKey = start; mStates[std::string{ groupname }] = state; if (state.mPlaying) @@ -921,7 +1068,7 @@ namespace MWRender return true; } - void Animation::setTextKeyListener(Animation::TextKeyListener* listener) + void Animation::setTextKeyListener(TextKeyListener* listener) { mTextKeyListener = listener; } @@ -930,13 +1077,64 @@ namespace MWRender { if (!mNodeMapCreated && mObjectRoot) { - SceneUtil::NodeMapVisitor visitor(mNodeMap); - mObjectRoot->accept(visitor); + // If the base of this animation is an osgAnimation, we should map the bones not matrix transforms + if (mRequiresBoneMap) + { + SceneUtil::NodeMapVisitorBoneOnly visitor(mNodeMap); + mObjectRoot->accept(visitor); + } + else + { + SceneUtil::NodeMapVisitor visitor(mNodeMap); + mObjectRoot->accept(visitor); + } mNodeMapCreated = true; } return mNodeMap; } + template + inline osg::Callback* Animation::handleBlendTransform(const osg::ref_ptr& node, + osg::ref_ptr keyframeController, + std::map, osg::ref_ptr>& blendControllers, + const AnimBlendStateData& stateData, const osg::ref_ptr& blendRules, + const AnimState& active) + { + osg::ref_ptr animController; + if (blendControllers.contains(node)) + { + animController = blendControllers.at(node); + animController->setKeyframeTrack(keyframeController, stateData, blendRules); + } + else + { + animController = new ControllerType(keyframeController, stateData, blendRules); + blendControllers.emplace(node, animController); + + if constexpr (std::is_same_v) + assignBoneBlendCallbackRecursive(animController, node, true); + } + + keyframeController->mTime = active.mTime; + + osg::Callback* asCallback = animController->getAsCallback(); + if constexpr (std::is_same_v) + { + // IMPORTANT: we must gather all transforms at point of change before next update + // instead of at the root update callback because the root bone may require blending. + if (animController->getBlendTrigger()) + animController->gatherRecursiveBoneTransforms(static_cast(node.get())); + + // Register blend callback after the initial animation callback + node->addUpdateCallback(asCallback); + mActiveControllers.emplace_back(node, asCallback); + + return keyframeController->getAsCallback(); + } + + return asCallback; + } + void Animation::resetActiveGroups() { // remove all previous external controllers from the scene graph @@ -960,7 +1158,7 @@ namespace MWRender AnimStateMap::const_iterator state = mStates.begin(); for (; state != mStates.end(); ++state) { - if (!(state->second.mBlendMask & (1 << blendMask))) + if (!state->second.blendMaskContains(blendMask)) continue; if (active == mStates.end() @@ -975,6 +1173,8 @@ namespace MWRender if (active != mStates.end()) { std::shared_ptr animsrc = active->second.mSource; + const AnimBlendStateData stateData + = { .mGroupname = active->second.mGroupname, .mStartKey = active->second.mStartKey }; for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); it != animsrc->mControllerMap[blendMask].end(); ++it) @@ -982,7 +1182,23 @@ namespace MWRender osg::ref_ptr node = getNodeMap().at( it->first); // this should not throw, we already checked for the node existing in addAnimSource + const bool useSmoothAnims = Settings::game().mSmoothAnimTransitions; + osg::Callback* callback = it->second->getAsCallback(); + if (useSmoothAnims) + { + if (dynamic_cast(node.get())) + { + callback = handleBlendTransform(node, it->second, + mAnimBlendControllers, stateData, animsrc->mAnimBlendRules, active->second); + } + else if (dynamic_cast(node.get())) + { + callback = handleBlendTransform(node, it->second, + mBoneAnimBlendControllers, stateData, animsrc->mAnimBlendRules, active->second); + } + } + node->addUpdateCallback(callback); mActiveControllers.emplace_back(node, callback); @@ -1002,6 +1218,7 @@ namespace MWRender } } } + addControllers(); } @@ -1020,7 +1237,7 @@ namespace MWRender return false; } - bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult) const + bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult, size_t* loopcount) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if (iter == mStates.end()) @@ -1029,6 +1246,8 @@ namespace MWRender *complete = 0.0f; if (speedmult) *speedmult = 0.0f; + if (loopcount) + *loopcount = 0; return false; } @@ -1042,10 +1261,22 @@ namespace MWRender } if (speedmult) *speedmult = iter->second.mSpeedMult; + + if (loopcount) + *loopcount = iter->second.mLoopCount; return true; } - float Animation::getCurrentTime(const std::string& groupname) const + std::string_view Animation::getActiveGroup(BoneGroup boneGroup) const + { + if (auto timePtr = mAnimationTimePtr[boneGroup]->getTimePtr()) + for (auto& state : mStates) + if (state.second.mTime == timePtr) + return state.first; + return ""; + } + + float Animation::getCurrentTime(std::string_view groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if (iter == mStates.end()) @@ -1054,15 +1285,6 @@ namespace MWRender return iter->second.getTime(); } - size_t Animation::getCurrentLoopCount(const std::string& groupname) const - { - AnimStateMap::const_iterator iter = mStates.find(groupname); - if (iter == mStates.end()) - return 0; - - return iter->second.mLoopCount; - } - void Animation::disable(std::string_view groupname) { AnimStateMap::iterator iter = mStates.find(groupname); @@ -1141,24 +1363,12 @@ namespace MWRender osg::Vec3f Animation::runAnimation(float duration) { - // If we have scripted animations, play only them - bool hasScriptedAnims = false; - for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) - { - if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent)) - && stateiter->second.mPlaying) - { - hasScriptedAnims = true; - break; - } - } - osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while (stateiter != mStates.end()) { AnimState& state = stateiter->second; - if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent))) + if (mPlayScriptedOnly && !state.mPriority.contains(MWMechanics::Priority_Scripted)) { ++stateiter; continue; @@ -1236,9 +1446,11 @@ namespace MWRender mRootController->setEnabled(enable); if (enable) { - mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)) - * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); + osg::Quat legYaw = osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)); + mRootController->setRotate(legYaw * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); yawOffset = mLegsYawRadians; + // When yawing the root, also update the accumulated movement. + movement = legYaw * movement; } } if (mSpineController) @@ -1262,10 +1474,6 @@ namespace MWRender osg::Quat(mHeadPitchRadians, osg::Vec3f(1, 0, 0)) * osg::Quat(yaw, osg::Vec3f(0, 0, 1))); } - // Scripted animations should not cause movement - if (hasScriptedAnims) - return osg::Vec3f(0, 0, 0); - return movement; } @@ -1277,7 +1485,7 @@ namespace MWRender } void loadBonesFromFile( - osg::ref_ptr& baseNode, const std::string& model, Resource::ResourceSystem* resourceSystem) + osg::ref_ptr& baseNode, VFS::Path::NormalizedView model, Resource::ResourceSystem* resourceSystem) { const osg::Node* node = resourceSystem->getSceneManager()->getTemplate(model).get(); osg::ref_ptr sheathSkeleton( @@ -1312,7 +1520,7 @@ namespace MWRender } animationPath.replace(animationPath.size() - 4, 4, "/"); - for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) + for (const VFS::Path::Normalized& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { if (Misc::getFileExtension(name) == "nif") loadBonesFromFile(node, name, resourceSystem); @@ -1330,7 +1538,7 @@ namespace MWRender Cache::iterator found = cache.find(model); if (found == cache.end()) { - osg::ref_ptr created = sceneMgr->getInstance(model); + osg::ref_ptr created = sceneMgr->getInstance(VFS::Path::toNormalized(model)); if (inject) { @@ -1351,7 +1559,7 @@ namespace MWRender } else { - osg::ref_ptr created = sceneMgr->getInstance(model); + osg::ref_ptr created = sceneMgr->getInstance(VFS::Path::toNormalized(model)); if (inject) { @@ -1394,7 +1602,7 @@ namespace MWRender MWWorld::LiveCellRef* ref = mPtr.get(); if (ref->mBase->mFlags & ESM::Creature::Bipedal) { - defaultSkeleton = Settings::Manager::getString("xbaseanim", "Models"); + defaultSkeleton = Settings::models().mXbaseanim.get().value(); inject = true; } } @@ -1411,12 +1619,14 @@ namespace MWRender const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Race* race = store.get().find(ref->mBase->mRace); - bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; - bool isFemale = !ref->mBase->isMale(); + const bool firstPerson = false; + const bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; + const bool isFemale = !ref->mBase->isMale(); + const bool werewolf = false; - defaultSkeleton = SceneUtil::getActorSkeleton(false, isFemale, isBeast, false); - defaultSkeleton - = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS()); + defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( + VFS::Path::toNormalized(getActorSkeleton(firstPerson, isFemale, isBeast, werewolf)), + mResourceSystem->getVFS()); } } } @@ -1453,6 +1663,10 @@ namespace MWRender mInsert->addChild(mObjectRoot); } + // osgAnimation formats with skeletons should have their nodemap be bone instances + // FIXME: better way to detect osgAnimation here instead of relying on extension? + mRequiresBoneMap = mSkeleton != nullptr && !Misc::StringUtils::ciEndsWith(model, ".nif"); + if (previousStateset) mObjectRoot->setStateSet(previousStateset); @@ -1485,7 +1699,7 @@ namespace MWRender return mObjectRoot.get(); } - void Animation::addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration) + void Animation::addSpellCastGlow(const osg::Vec4f& color, float glowDuration) { if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true))) { @@ -1494,12 +1708,11 @@ namespace MWRender if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) { - mGlowUpdater->setColor(effect->getColor()); + mGlowUpdater->setColor(color); mGlowUpdater->setDuration(glowDuration); } else - mGlowUpdater - = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, effect->getColor(), glowDuration); + mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration); } } @@ -1511,8 +1724,8 @@ namespace MWRender mExtraLightSource->setActorFade(mAlpha); } - void Animation::addEffect( - const std::string& model, int effectId, bool loop, std::string_view bonename, std::string_view texture) + void Animation::addEffect(std::string_view model, std::string_view effectId, bool loop, std::string_view bonename, + std::string_view texture) { if (!mObjectRoot.get()) return; @@ -1562,13 +1775,13 @@ namespace MWRender } parentNode->addChild(trans); - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model, trans); + osg::ref_ptr node + = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(model), trans); // Morrowind has a white ambient light attached to the root VFX node of the scenegraph node->getOrCreateStateSet()->setAttributeAndModes( getVFXLightModelInstance(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); - if (mResourceSystem->getSceneManager()->getSupportsNormalsRT()) - node->getOrCreateStateSet()->setAttribute(new osg::ColorMaski(1, false, false, false, false)); + mResourceSystem->getSceneManager()->setUpNormalsRTForStateSet(node->getOrCreateStateSet(), false); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); @@ -1591,10 +1804,10 @@ namespace MWRender // Notify that this animation has attached magic effects mHasMagicEffects = true; - overrideFirstRootTexture(texture, mResourceSystem, node); + overrideFirstRootTexture(texture, mResourceSystem, *node); } - void Animation::removeEffect(int effectId) + void Animation::removeEffect(std::string_view effectId) { RemoveCallbackVisitor visitor(effectId); mInsert->accept(visitor); @@ -1604,17 +1817,19 @@ namespace MWRender void Animation::removeEffects() { - removeEffect(-1); + removeEffect(""); } - void Animation::getLoopingEffects(std::vector& out) const + std::vector Animation::getLoopingEffects() const { if (!mHasMagicEffects) - return; + return {}; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); + std::vector out; + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { @@ -1623,6 +1838,7 @@ namespace MWRender if (callback->mParams.mLoop && !callback->mFinished) out.push_back(callback->mParams.mEffectId); } + return out; } void Animation::updateEffects() @@ -1749,13 +1965,15 @@ namespace MWRender osg::Callback* cb = node->getUpdateCallback(); while (cb) { - if (dynamic_cast(cb)) + if (dynamic_cast(cb) || dynamic_cast(cb) + || dynamic_cast(cb)) { foundKeyframeCtrl = true; break; } cb = cb->getNestedCallback(); } + // Note: AnimBlendController also does the reset so if one is present - we should add the rotation node // Without KeyframeController the orientation will not be reseted each frame, so // RotateController shouldn't be used for such nodes. if (!foundKeyframeCtrl) @@ -1886,7 +2104,8 @@ namespace MWRender mObjectRoot->accept(visitor); } - if (ptr.getRefData().getCustomData() != nullptr && ObjectAnimation::canBeHarvested()) + if (Settings::game().mGraphicHerbalism && ptr.getRefData().getCustomData() != nullptr + && ObjectAnimation::canBeHarvested()) { const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); if (!store.hasVisibleItems()) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 8615811cc3..d398c5b727 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -1,16 +1,25 @@ #ifndef GAME_RENDER_ANIMATION_H #define GAME_RENDER_ANIMATION_H +#include "animationpriority.hpp" +#include "animblendcontroller.hpp" +#include "blendmask.hpp" +#include "bonegroup.hpp" + #include "../mwworld/movementdirection.hpp" #include "../mwworld/ptr.hpp" #include +#include #include #include #include #include +#include +#include #include +#include #include #include #include @@ -43,6 +52,8 @@ namespace MWRender class RotateController; class TransparencyUpdater; + using ActiveControllersVector = std::vector, osg::ref_ptr>>; + class EffectAnimationTime : public SceneUtil::ControllerSource { private: @@ -84,7 +95,7 @@ namespace MWRender std::string mModelName; // Just here so we don't add the same effect twice std::shared_ptr mAnimTime; float mMaxControllerLength; - int mEffectId; + std::string mEffectId; bool mLoop; std::string mBoneName; }; @@ -92,60 +103,9 @@ namespace MWRender class Animation : public osg::Referenced { public: - enum BoneGroup - { - BoneGroup_LowerBody = 0, - BoneGroup_Torso, - BoneGroup_LeftArm, - BoneGroup_RightArm - }; - - enum BlendMask - { - BlendMask_LowerBody = 1 << 0, - BlendMask_Torso = 1 << 1, - BlendMask_LeftArm = 1 << 2, - BlendMask_RightArm = 1 << 3, - - BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, - - BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody - }; - /* This is the number of *discrete* blend masks. */ - static constexpr size_t sNumBlendMasks = 4; - - /// Holds an animation priority value for each BoneGroup. - struct AnimPriority - { - /// Convenience constructor, initialises all priorities to the same value. - AnimPriority(int priority) - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - mPriority[i] = priority; - } - - bool operator==(const AnimPriority& other) const - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - if (other.mPriority[i] != mPriority[i]) - return false; - return true; - } - - int& operator[](BoneGroup n) { return mPriority[n]; } - - const int& operator[](BoneGroup n) const { return mPriority[n]; } - - bool contains(int priority) const - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - if (priority == mPriority[i]) - return true; - return false; - } - - int mPriority[sNumBlendMasks]; - }; + using BlendMask = MWRender::BlendMask; + using BoneGroup = MWRender::BoneGroup; + using AnimPriority = MWRender::AnimPriority; class TextKeyListener { @@ -189,45 +149,31 @@ namespace MWRender struct AnimState { std::shared_ptr mSource; - float mStartTime; - float mLoopStartTime; - float mLoopStopTime; - float mStopTime; + float mStartTime = 0; + float mLoopStartTime = 0; + float mLoopStopTime = 0; + float mStopTime = 0; - typedef std::shared_ptr TimePtr; - TimePtr mTime; - float mSpeedMult; + std::shared_ptr mTime = std::make_shared(0); + float mSpeedMult = 1; - bool mPlaying; - bool mLoopingEnabled; - size_t mLoopCount; + bool mPlaying = false; + bool mLoopingEnabled = true; + uint32_t mLoopCount = 0; - AnimPriority mPriority; - int mBlendMask; - bool mAutoDisable; + AnimPriority mPriority{ 0 }; + int mBlendMask = 0; + bool mAutoDisable = true; - AnimState() - : mStartTime(0.0f) - , mLoopStartTime(0.0f) - , mLoopStopTime(0.0f) - , mStopTime(0.0f) - , mTime(new float) - , mSpeedMult(1.0f) - , mPlaying(false) - , mLoopingEnabled(true) - , mLoopCount(0) - , mPriority(0) - , mBlendMask(0) - , mAutoDisable(true) - { - } - ~AnimState() = default; + std::string mGroupname; + std::string mStartKey; float getTime() const { return *mTime; } void setTime(float time) { *mTime = time; } - + bool blendMaskContains(size_t blendMask) const { return (mBlendMask & (1 << blendMask)); } bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; } }; + typedef std::map> AnimStateMap; AnimStateMap mStates; @@ -253,7 +199,11 @@ namespace MWRender // Keep track of controllers that we added to our scene graph. // We may need to rebuild these controllers when the active animation groups / sources change. - std::vector, osg::ref_ptr>> mActiveControllers; + ActiveControllersVector mActiveControllers; + + // Keep track of the animation controllers for easy access + std::map, osg::ref_ptr> mAnimBlendControllers; + std::map, osg::ref_ptr> mBoneAnimBlendControllers; std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; @@ -292,9 +242,14 @@ namespace MWRender osg::ref_ptr mLightListCallback; + bool mPlayScriptedOnly; + bool mRequiresBoneMap; + const NodeMap& getNodeMap() const; - /* Sets the appropriate animations on the bone groups based on priority. + /* Sets the appropriate animations on the bone groups based on priority by finding + * the highest priority AnimationStates and linking the appropriate controllers stored + * in the AnimationState to the corresponding nodes. */ void resetActiveGroups(); @@ -327,7 +282,7 @@ namespace MWRender */ void setObjectRoot(const std::string& model, bool forceskeleton, bool baseonly, bool isCreature); - void loadAllAnimationsInFolder(const std::string& model, const std::string& baseModel); + void loadAdditionalAnimations(VFS::Path::NormalizedView model, const std::string& baseModel); /** Adds the keyframe controllers in the specified model as a new animation source. * @note Later added animation sources have the highest priority when it comes to finding a particular @@ -336,7 +291,7 @@ namespace MWRender * @param baseModel The filename of the mObjectRoot, only used for error messages. */ void addAnimSource(std::string_view model, const std::string& baseModel); - void addSingleAnimSource(const std::string& model, const std::string& baseModel); + std::shared_ptr addSingleAnimSource(const std::string& model, const std::string& baseModel); /** Adds an additional light to the given node using the specified ESM record. */ void addExtraLight(osg::ref_ptr parent, const SceneUtil::LightCommon& light); @@ -352,6 +307,13 @@ namespace MWRender void removeFromSceneImpl(); + template + inline osg::Callback* handleBlendTransform(const osg::ref_ptr& node, + osg::ref_ptr keyframeController, + std::map, osg::ref_ptr>& blendControllers, + const AnimBlendStateData& stateData, const osg::ref_ptr& blendRules, + const AnimState& active); + public: Animation( const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); @@ -382,26 +344,29 @@ namespace MWRender * @param texture override the texture specified in the model's materials - if empty, do not override * @note Will not add an effect twice. */ - void addEffect(const std::string& model, int effectId, bool loop = false, std::string_view bonename = {}, - std::string_view texture = {}); - void removeEffect(int effectId); + void addEffect(std::string_view model, std::string_view effectId, bool loop = false, + std::string_view bonename = {}, std::string_view texture = {}); + void removeEffect(std::string_view effectId); void removeEffects(); - void getLoopingEffects(std::vector& out) const; + std::vector getLoopingEffects() const; // Add a spell casting glow to an object. From measuring video taken from the original engine, // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. - void addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration = 1.5); + void addSpellCastGlow(const osg::Vec4f& color, float glowDuration = 1.5); virtual void updatePtr(const MWWorld::Ptr& ptr); bool hasAnimation(std::string_view anim) const; + bool isLoopingAnimation(std::string_view group) const; + // Specifies the axis' to accumulate on. Non-accumulated axis will just // move visually, but not affect the actual movement. Each x/y/z value // should be on the scale of 0 to 1. void setAccumulation(const osg::Vec3f& accum); /** Plays an animation. + * Creates or updates AnimationStates to represent and manage animation playback. * \param groupname Name of the animation group to play. * \param priority Priority of the animation. The animation will play on * bone groups that don't have another animation set of a @@ -422,7 +387,7 @@ namespace MWRender * the "start" and "stop" keys for looping? */ void play(std::string_view groupname, const AnimPriority& priority, int blendMask, bool autodisable, - float speedmult, std::string_view start, std::string_view stop, float startpoint, size_t loops, + float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback = false); /** Adjust the speed multiplier of an already playing animation. @@ -441,7 +406,11 @@ namespace MWRender * \param speedmult Stores the animation speed multiplier * \return True if the animation is active, false otherwise. */ - bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr) const; + bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr, + size_t* loopcount = nullptr) const; + + /// Returns the group name of the animation currently active on that bone group. + std::string_view getActiveGroup(BoneGroup boneGroup) const; /// Get the absolute position in the animation track of the first text key with the given group. float getStartTime(const std::string& groupname) const; @@ -451,9 +420,7 @@ namespace MWRender /// Get the current absolute position in the animation track for the animation that is currently playing from /// the given group. - float getCurrentTime(const std::string& groupname) const; - - size_t getCurrentLoopCount(const std::string& groupname) const; + float getCurrentTime(std::string_view groupname) const; /** Disables the specified animation group; * \param groupname Animation group to disable. @@ -477,6 +444,9 @@ namespace MWRender MWWorld::MovementDirectionFlags getSupportedMovementDirections( std::span prefixes) const; + bool getPlayScriptedOnly() const { return mPlayScriptedOnly; } + void setPlayScriptedOnly(bool playScriptedOnly) { mPlayScriptedOnly = playScriptedOnly; } + virtual bool useShieldAnimations() const { return false; } virtual bool getWeaponsShown() const { return false; } virtual void showWeapons(bool showWeapon) {} @@ -545,6 +515,5 @@ namespace MWRender private: double mStartingTime; }; - } #endif diff --git a/apps/openmw/mwrender/animationpriority.hpp b/apps/openmw/mwrender/animationpriority.hpp new file mode 100644 index 0000000000..048d29901e --- /dev/null +++ b/apps/openmw/mwrender/animationpriority.hpp @@ -0,0 +1,42 @@ +#ifndef GAME_RENDER_ANIMATIONPRIORITY_H +#define GAME_RENDER_ANIMATIONPRIORITY_H + +#include "blendmask.hpp" +#include "bonegroup.hpp" + +namespace MWRender +{ + /// Holds an animation priority value for each BoneGroup. + struct AnimPriority + { + /// Convenience constructor, initialises all priorities to the same value. + AnimPriority(int priority) + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + mPriority[i] = priority; + } + + bool operator==(const AnimPriority& other) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (other.mPriority[i] != mPriority[i]) + return false; + return true; + } + + int& operator[](BoneGroup n) { return mPriority[n]; } + + const int& operator[](BoneGroup n) const { return mPriority[n]; } + + bool contains(int priority) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (priority == mPriority[i]) + return true; + return false; + } + + int mPriority[sNumBlendMasks]; + }; +} +#endif diff --git a/apps/openmw/mwrender/animblendcontroller.cpp b/apps/openmw/mwrender/animblendcontroller.cpp new file mode 100644 index 0000000000..3d8b7b59d7 --- /dev/null +++ b/apps/openmw/mwrender/animblendcontroller.cpp @@ -0,0 +1,393 @@ +#include "animblendcontroller.hpp" +#include "rotatecontroller.hpp" + +#include + +#include + +#include +#include +#include + +namespace MWRender +{ + namespace + { + // Animation Easing/Blending functions + namespace Easings + { + float linear(float x) + { + return x; + } + + float sineOut(float x) + { + return std::sin((x * osg::PIf) / 2.f); + } + + float sineIn(float x) + { + return 1.f - std::cos((x * osg::PIf) / 2.f); + } + + float sineInOut(float x) + { + return -(std::cos(osg::PIf * x) - 1.f) / 2.f; + } + + float cubicOut(float t) + { + float t1 = 1.f - t; + return 1.f - (t1 * t1 * t1); // (1-t)^3 + } + + float cubicIn(float x) + { + return x * x * x; // x^3 + } + + float cubicInOut(float x) + { + if (x < 0.5f) + { + return 4.f * x * x * x; // 4x^3 + } + else + { + float x2 = -2.f * x + 2.f; + return 1.f - (x2 * x2 * x2) / 2.f; // (1 - (-2x + 2)^3)/2 + } + } + + float quartOut(float t) + { + float t1 = 1.f - t; + return 1.f - (t1 * t1 * t1 * t1); // (1-t)^4 + } + + float quartIn(float t) + { + return t * t * t * t; // t^4 + } + + float quartInOut(float x) + { + if (x < 0.5f) + { + return 8.f * x * x * x * x; // 8x^4 + } + else + { + float x2 = -2.f * x + 2.f; + return 1.f - (x2 * x2 * x2 * x2) / 2.f; // 1 - ((-2x + 2)^4)/2 + } + } + + float springOutGeneric(float x, float lambda) + { + // Higher lambda = lower swing amplitude. 1 = 150% swing amplitude. + // w is the frequency of oscillation in the easing func, controls the amount of overswing + const float w = 1.5f * osg::PIf; // 4.71238 + return 1.f - expf(-lambda * x) * std::cos(w * x); + } + + float springOutWeak(float x) + { + return springOutGeneric(x, 4.f); + } + + float springOutMed(float x) + { + return springOutGeneric(x, 3.f); + } + + float springOutStrong(float x) + { + return springOutGeneric(x, 2.f); + } + + float springOutTooMuch(float x) + { + return springOutGeneric(x, 1.f); + } + + const std::unordered_map easingsMap = { + { "linear", Easings::linear }, + { "sineOut", Easings::sineOut }, + { "sineIn", Easings::sineIn }, + { "sineInOut", Easings::sineInOut }, + { "cubicOut", Easings::cubicOut }, + { "cubicIn", Easings::cubicIn }, + { "cubicInOut", Easings::cubicInOut }, + { "quartOut", Easings::quartOut }, + { "quartIn", Easings::quartIn }, + { "quartInOut", Easings::quartInOut }, + { "springOutWeak", Easings::springOutWeak }, + { "springOutMed", Easings::springOutMed }, + { "springOutStrong", Easings::springOutStrong }, + { "springOutTooMuch", Easings::springOutTooMuch }, + }; + } + + osg::Vec3f vec3fLerp(float t, const osg::Vec3f& start, const osg::Vec3f& end) + { + return start + (end - start) * t; + } + } + + AnimBlendController::AnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + : mEasingFn(&Easings::sineOut) + { + setKeyframeTrack(keyframeTrack, newState, blendRules); + } + + AnimBlendController::AnimBlendController() + : mEasingFn(&Easings::sineOut) + { + } + + NifAnimBlendController::NifAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + : AnimBlendController(keyframeTrack, newState, blendRules) + { + } + + BoneAnimBlendController::BoneAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + : AnimBlendController(keyframeTrack, newState, blendRules) + { + } + + void AnimBlendController::setKeyframeTrack(const osg::ref_ptr& kft, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + { + // If animation has changed then start blending + if (newState.mGroupname != mAnimState.mGroupname || newState.mStartKey != mAnimState.mStartKey + || kft != mKeyframeTrack) + { + // Default blend settings + mBlendDuration = 0; + mEasingFn = &Easings::sineOut; + + if (blendRules) + { + // Finds a matching blend rule either in this or previous ruleset + auto blendRule = blendRules->findBlendingRule( + mAnimState.mGroupname, mAnimState.mStartKey, newState.mGroupname, newState.mStartKey); + + if (blendRule) + { + if (const auto it = Easings::easingsMap.find(blendRule->mEasing); it != Easings::easingsMap.end()) + { + mEasingFn = it->second; + mBlendDuration = blendRule->mDuration; + } + else + { + Log(Debug::Warning) + << "Warning: animation blending rule contains invalid easing type: " << blendRule->mEasing; + } + } + } + + mAnimBlendRules = blendRules; + mKeyframeTrack = kft; + mAnimState = newState; + mBlendTrigger = true; + } + } + + void AnimBlendController::calculateInterpFactor(float time) + { + if (mBlendDuration != 0) + mTimeFactor = std::min((time - mBlendStartTime) / mBlendDuration, 1.0f); + else + mTimeFactor = 1; + + mInterpActive = mTimeFactor < 1.0; + + if (mInterpActive) + mInterpFactor = mEasingFn(mTimeFactor); + else + mInterpFactor = 1.0f; + } + + void BoneAnimBlendController::gatherRecursiveBoneTransforms(osgAnimation::Bone* bone, bool isRoot) + { + // Incase group traversal encountered something that isnt a bone + if (!bone) + return; + + mBlendBoneTransforms[bone] = bone->getMatrix(); + + osg::Group* group = bone->asGroup(); + if (group) + { + for (unsigned int i = 0; i < group->getNumChildren(); ++i) + gatherRecursiveBoneTransforms(dynamic_cast(group->getChild(i)), false); + } + } + + void BoneAnimBlendController::applyBoneBlend(osgAnimation::Bone* bone) + { + // If we are done with interpolation then we can safely skip this as the bones are correct + if (!mInterpActive) + return; + + // Shouldn't happen, but potentially an edge case where a new bone was added + // between gatherRecursiveBoneTransforms and this update + // currently OpenMW will never do this + assert(mBlendBoneTransforms.find(bone) != mBlendBoneTransforms.end()); + + // Every frame the osgAnimation controller updates this + // so it is ok that we update it directly below + const osg::Matrixf& currentSampledMatrix = bone->getMatrix(); + const osg::Matrixf& lastSampledMatrix = mBlendBoneTransforms.at(bone); + + const osg::Vec3f scale = currentSampledMatrix.getScale(); + const osg::Quat rotation = currentSampledMatrix.getRotate(); + const osg::Vec3f translation = currentSampledMatrix.getTrans(); + + const osg::Quat blendRotation = lastSampledMatrix.getRotate(); + const osg::Vec3f blendTrans = lastSampledMatrix.getTrans(); + + osg::Quat lerpedRot; + lerpedRot.slerp(mInterpFactor, blendRotation, rotation); + + osg::Matrixf lerpedMatrix; + lerpedMatrix.makeRotate(lerpedRot); + lerpedMatrix.setTrans(vec3fLerp(mInterpFactor, blendTrans, translation)); + + // Scale is not lerped based on the idea that it is much more likely that scale animation will be used to + // instantly hide/show objects in which case the scale interpolation is undesirable. + lerpedMatrix = osg::Matrixd::scale(scale) * lerpedMatrix; + + // Apply new blended matrix + osgAnimation::Bone* boneParent = bone->getBoneParent(); + bone->setMatrix(lerpedMatrix); + if (boneParent) + bone->setMatrixInSkeletonSpace(lerpedMatrix * boneParent->getMatrixInSkeletonSpace()); + else + bone->setMatrixInSkeletonSpace(lerpedMatrix); + } + + void BoneAnimBlendController::operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv) + { + // HOW THIS WORKS: This callback method is called only for bones with attached keyframe controllers + // such as bip01, bip01 spine1 etc. The child bones of these controllers have their own callback wrapper + // which will call this instance's applyBoneBlend for each child bone. The order of update is important + // as the blending calculations expect the bone's skeleton matrix to be at the sample point + float time = nv->getFrameStamp()->getSimulationTime(); + assert(node != nullptr); + + if (mBlendTrigger) + { + mBlendTrigger = false; + mBlendStartTime = time; + } + + calculateInterpFactor(time); + + if (mInterpActive) + applyBoneBlend(node); + + SceneUtil::NodeCallback::traverse(node, nv); + } + + void NifAnimBlendController::operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) + { + // HOW THIS WORKS: The actual retrieval of the bone transformation based on animation is done by the + // KeyframeController (mKeyframeTrack). The KeyframeController retreives time data (playback position) every + // frame from controller's input (getInputValue(nv)) which is bound to an appropriate AnimationState time value + // in Animation.cpp. Animation.cpp ultimately manages animation playback via updating AnimationState objects and + // determines when and what should be playing. + // This controller exploits KeyframeController to get transformations and upon animation change blends from + // the last known position to the new animated one. + + auto [translation, rotation, scale] = mKeyframeTrack->getCurrentTransformation(nv); + + float time = nv->getFrameStamp()->getSimulationTime(); + + if (mBlendTrigger) + { + mBlendTrigger = false; + mBlendStartTime = time; + + // Nif mRotationScale is used here because it's unaffected by the side-effects of RotationController + mBlendStartRot = node->mRotationScale.toOsgMatrix().getRotate(); + mBlendStartTrans = node->getMatrix().getTrans(); + mBlendStartScale = node->mScale; + + // Subtract any rotate controller's offset from start transform (if it appears after this callback) + // this is required otherwise the blend start will be with an offset, then offset could be applied again + // fixes an issue with camera jumping during first person sneak jumping camera + osg::Callback* updateCb = node->getUpdateCallback()->getNestedCallback(); + while (updateCb) + { + MWRender::RotateController* rotateController = dynamic_cast(updateCb); + if (rotateController) + { + const osg::Quat& rotate = rotateController->getRotate(); + const osg::Vec3f& offset = rotateController->getOffset(); + + osg::NodePathList nodepaths = node->getParentalNodePaths(rotateController->getRelativeTo()); + osg::Quat worldOrient; + if (!nodepaths.empty()) + { + osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); + worldOrient = worldMat.getRotate(); + } + + worldOrient = worldOrient * rotate.inverse(); + const osg::Quat worldOrientInverse = worldOrient.inverse(); + + mBlendStartTrans -= worldOrientInverse * offset; + } + + updateCb = updateCb->getNestedCallback(); + } + } + + calculateInterpFactor(time); + + if (mInterpActive) + { + if (rotation) + { + osg::Quat lerpedRot; + lerpedRot.slerp(mInterpFactor, mBlendStartRot, *rotation); + node->setRotation(lerpedRot); + } + else + { + // This is necessary to prevent first person animation glitching out + node->setRotation(node->mRotationScale); + } + + if (translation) + { + osg::Vec3f lerpedTrans = vec3fLerp(mInterpFactor, mBlendStartTrans, *translation); + node->setTranslation(lerpedTrans); + } + } + else + { + if (translation) + node->setTranslation(*translation); + + if (rotation) + node->setRotation(*rotation); + else + node->setRotation(node->mRotationScale); + } + + if (scale) + // Scale is not lerped based on the idea that it is much more likely that scale animation will be used to + // instantly hide/show objects in which case the scale interpolation is undesirable. + node->setScale(*scale); + + SceneUtil::NodeCallback::traverse(node, nv); + } +} diff --git a/apps/openmw/mwrender/animblendcontroller.hpp b/apps/openmw/mwrender/animblendcontroller.hpp new file mode 100644 index 0000000000..40a5d7582f --- /dev/null +++ b/apps/openmw/mwrender/animblendcontroller.hpp @@ -0,0 +1,142 @@ +#ifndef OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H +#define OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace MWRender +{ + typedef float (*EasingFn)(float); + + struct AnimBlendStateData + { + std::string mGroupname; + std::string mStartKey; + }; + + class AnimBlendController : public SceneUtil::Controller + { + public: + AnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + AnimBlendController(); + + void setKeyframeTrack(const osg::ref_ptr& kft, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + bool getBlendTrigger() const { return mBlendTrigger; } + + protected: + EasingFn mEasingFn; + float mBlendDuration = 0.0f; + float mBlendStartTime = 0.0f; + float mTimeFactor = 0.0f; + float mInterpFactor = 0.0f; + + bool mBlendTrigger = false; + bool mInterpActive = false; + + AnimBlendStateData mAnimState; + osg::ref_ptr mAnimBlendRules; + osg::ref_ptr mKeyframeTrack; + + std::unordered_map mBlendBoneTransforms; + + inline void calculateInterpFactor(float time); + }; + + class NifAnimBlendController : public SceneUtil::NodeCallback, + public AnimBlendController + { + public: + NifAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + NifAnimBlendController() {} + + NifAnimBlendController(const NifAnimBlendController& other, const osg::CopyOp&) + : NifAnimBlendController(other.mKeyframeTrack, other.mAnimState, other.mAnimBlendRules) + { + } + + META_Object(MWRender, NifAnimBlendController) + + void operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv); + + osg::Callback* getAsCallback() { return this; } + + private: + osg::Quat mBlendStartRot; + osg::Vec3f mBlendStartTrans; + float mBlendStartScale = 0.0f; + }; + + class BoneAnimBlendController : public SceneUtil::NodeCallback, + public AnimBlendController + { + public: + BoneAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + BoneAnimBlendController() {} + + BoneAnimBlendController(const BoneAnimBlendController& other, const osg::CopyOp&) + : BoneAnimBlendController(other.mKeyframeTrack, other.mAnimState, other.mAnimBlendRules) + { + } + + void gatherRecursiveBoneTransforms(osgAnimation::Bone* parent, bool isRoot = true); + void applyBoneBlend(osgAnimation::Bone* parent); + + META_Object(MWRender, BoneAnimBlendController) + + void operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv); + + osg::Callback* getAsCallback() { return this; } + }; + + // Assigned to child bones with an instance of AnimBlendController + class BoneAnimBlendControllerWrapper : public osg::Callback + { + public: + BoneAnimBlendControllerWrapper(osg::ref_ptr rootCallback, osgAnimation::Bone* node) + : mRootCallback(std::move(rootCallback)) + , mNode(node) + { + } + + BoneAnimBlendControllerWrapper() {} + + BoneAnimBlendControllerWrapper(const BoneAnimBlendControllerWrapper& copy, const osg::CopyOp&) + : mRootCallback(copy.mRootCallback) + , mNode(copy.mNode) + { + } + + META_Object(MWRender, BoneAnimBlendControllerWrapper) + + bool run(osg::Object* object, osg::Object* data) override + { + mRootCallback->applyBoneBlend(mNode); + traverse(object, data); + return true; + } + + private: + osg::ref_ptr mRootCallback; + osgAnimation::Bone* mNode{ nullptr }; + }; +} + +#endif diff --git a/apps/openmw/mwrender/blendmask.hpp b/apps/openmw/mwrender/blendmask.hpp new file mode 100644 index 0000000000..f140814d8d --- /dev/null +++ b/apps/openmw/mwrender/blendmask.hpp @@ -0,0 +1,22 @@ +#ifndef GAME_RENDER_BLENDMASK_H +#define GAME_RENDER_BLENDMASK_H + +#include + +namespace MWRender +{ + enum BlendMask + { + BlendMask_LowerBody = 1 << 0, + BlendMask_Torso = 1 << 1, + BlendMask_LeftArm = 1 << 2, + BlendMask_RightArm = 1 << 3, + + BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, + + BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody + }; + /* This is the number of *discrete* blend masks. */ + static constexpr size_t sNumBlendMasks = 4; +} +#endif diff --git a/apps/openmw/mwrender/bonegroup.hpp b/apps/openmw/mwrender/bonegroup.hpp new file mode 100644 index 0000000000..2afedade86 --- /dev/null +++ b/apps/openmw/mwrender/bonegroup.hpp @@ -0,0 +1,16 @@ +#ifndef GAME_RENDER_BONEGROUP_H +#define GAME_RENDER_BONEGROUP_H + +namespace MWRender +{ + enum BoneGroup + { + BoneGroup_LowerBody = 0, + BoneGroup_Torso, + BoneGroup_LeftArm, + BoneGroup_RightArm, + + Num_BoneGroups + }; +} +#endif diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index fbe5c6b4c7..1c163a6701 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -109,8 +109,7 @@ namespace MWRender void Camera::updateCamera(osg::Camera* cam) { - osg::Quat orient = osg::Quat(mRoll + mExtraRoll, osg::Vec3d(0, 1, 0)) - * osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1)); + osg::Quat orient = getOrient(); osg::Vec3d forward = orient * osg::Vec3d(0, 1, 0); osg::Vec3d up = orient * osg::Vec3d(0, 0, 1); @@ -138,14 +137,12 @@ namespace MWRender if (mProcessViewChange) processViewChange(); - if (paused) - return; - // only show the crosshair in game mode MWBase::WindowManager* wm = MWBase::Environment::get().getWindowManager(); wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair); - updateFocalPointOffset(duration); + if (!paused) + updateFocalPointOffset(duration); updatePosition(); } @@ -211,6 +208,12 @@ namespace MWRender mPosition = focal + offset; } + osg::Quat Camera::getOrient() const + { + return osg::Quat(mRoll + mExtraRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) + * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1)); + } + void Camera::setMode(Mode newMode, bool force) { if (mMode == newMode) diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index c6500160fd..e09a265293 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -72,6 +72,8 @@ namespace MWRender void setExtraYaw(float angle) { mExtraYaw = angle; } void setExtraRoll(float angle) { mExtraRoll = angle; } + osg::Quat getOrient() const; + /// @param Force view mode switch, even if currently not allowed by the animation. void toggleViewMode(bool force = false); bool toggleVanityMode(bool enable); diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 5357f5025c..123eadfdec 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include "../mwworld/class.hpp" @@ -34,6 +34,7 @@ #include "../mwmechanics/weapontype.hpp" #include "npcanimation.hpp" +#include "util.hpp" #include "vismask.hpp" namespace MWRender @@ -153,8 +154,8 @@ namespace MWRender public: CharacterPreviewRTTNode(uint32_t sizeX, uint32_t sizeY) - : RTTNode(sizeX, sizeY, Settings::Manager::getInt("antialiasing", "Video"), false, 0, - StereoAwareness::Unaware_MultiViewShaders) + : RTTNode(sizeX, sizeY, Settings::video().mAntialiasing, false, 0, + StereoAwareness::Unaware_MultiViewShaders, shouldAddMSAAIntermediateTarget()) , mAspectRatio(static_cast(sizeX) / static_cast(sizeY)) { if (SceneUtil::AutoDepth::isReversed()) @@ -226,9 +227,13 @@ namespace MWRender mRTTNode = new CharacterPreviewRTTNode(sizeX, sizeY); mRTTNode->setNodeMask(Mask_RenderToTexture); - bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP; - - osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); + osg::ref_ptr lightManager = new SceneUtil::LightManager(SceneUtil::LightSettings{ + .mLightingMethod = mResourceSystem->getSceneManager()->getLightingMethod(), + .mMaxLights = Settings::shaders().mMaxLights, + .mMaximumLightDistance = Settings::shaders().mMaximumLightDistance, + .mLightFadeStart = Settings::shaders().mLightFadeStart, + .mLightBoundsMultiplier = Settings::shaders().mLightBoundsMultiplier, + }); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); stateset->setDefine("FORCE_OPAQUE", "1", osg::StateAttribute::ON); @@ -242,7 +247,7 @@ namespace MWRender defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); stateset->setAttribute(defaultMat); - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*stateset); // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) @@ -431,7 +436,7 @@ namespace MWRender // We still should use one-handed animation as fallback if (mAnimation->hasAnimation(inventoryGroup)) - groupname = inventoryGroup; + groupname = std::move(inventoryGroup); else { static const std::string oneHandFallback @@ -451,15 +456,15 @@ namespace MWRender mAnimation->showCarriedLeft(showCarriedLeft); - mCurrentAnimGroup = groupname; - mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mCurrentAnimGroup = std::move(groupname); + mAnimation->play(mCurrentAnimGroup, 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId && showCarriedLeft) { if (!mAnimation->getInfo("torch")) - mAnimation->play( - "torch", 2, Animation::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, ~0ul, true); + mAnimation->play("torch", 2, BlendMask::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, + std::numeric_limits::max(), true); } else if (mAnimation->getInfo("torch")) mAnimation->disable("torch"); @@ -528,7 +533,7 @@ namespace MWRender : CharacterPreview( parent, resourceSystem, MWMechanics::getPlayer(), 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0, 0, 8)) , mBase(*mCharacter.get()->mBase) - , mRef(&mBase) + , mRef(ESM::makeBlankCellRef(), &mBase) , mPitchRadians(osg::DegreesToRadians(6.f)) { mCharacter = MWWorld::Ptr(&mRef, nullptr); @@ -586,7 +591,7 @@ namespace MWRender void RaceSelectionPreview::onSetup() { CharacterPreview::onSetup(); - mAnimation->play("idle", 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mAnimation->play("idle", 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); mAnimation->runAnimation(0.f); // attach camera to follow the head node diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 77faee7231..f84e58e4bc 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -5,9 +5,10 @@ #include #include #include +#include #include #include -#include +#include #include "../mwmechanics/weapontype.hpp" @@ -27,7 +28,7 @@ namespace MWRender setObjectRoot(model, false, false, true); if ((ref->mBase->mFlags & ESM::Creature::Bipedal)) - addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); + addAnimSource(Settings::models().mXbaseanim.get(), model); if (animated) addAnimSource(model, model); @@ -47,7 +48,7 @@ namespace MWRender setObjectRoot(model, true, false, true); if ((ref->mBase->mFlags & ESM::Creature::Bipedal)) - addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); + addAnimSource(Settings::models().mXbaseanim.get(), model); if (animated) addAnimSource(model, model); @@ -110,7 +111,7 @@ namespace MWRender MWWorld::ConstPtr item = *it; std::string_view bonename; - std::string itemModel = item.getClass().getModel(item); + VFS::Path::Normalized itemModel = item.getClass().getCorrectedModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { if (item.getType() == ESM::Weapon::sRecordId) @@ -168,10 +169,13 @@ namespace MWRender if (slot == MWWorld::InventoryStore::Slot_CarriedRight) source = mWeaponAnimationTime; else - source = std::make_shared(); + source = mAnimationTimePtr[0]; - SceneUtil::AssignControllerSourcesVisitor assignVisitor(source); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::move(source)); attached->accept(assignVisitor); + + if (item.getType() == ESM::Light::sRecordId) + addExtraLight(scene->getNode()->asGroup(), SceneUtil::LightCommon(*item.get()->mBase)); } catch (std::exception& e) { diff --git a/apps/openmw/mwrender/distortion.cpp b/apps/openmw/mwrender/distortion.cpp new file mode 100644 index 0000000000..5df2bfb703 --- /dev/null +++ b/apps/openmw/mwrender/distortion.cpp @@ -0,0 +1,37 @@ +#include "distortion.hpp" + +#include + +#include "postprocessor.hpp" + +namespace MWRender +{ + void DistortionCallback::drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) + { + osg::State* state = renderInfo.getState(); + size_t frameId = state->getFrameStamp()->getFrameNumber() % 2; + + PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); + + if (!postProcessor || bin->getStage()->getFrameBufferObject() != postProcessor->getPrimaryFbo(frameId)) + return; + + mFBO[frameId]->apply(*state); + + const osg::Texture* tex + = mFBO[frameId]->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture(); + + glViewport(0, 0, tex->getTextureWidth(), tex->getTextureHeight()); + glClearColor(0.0, 0.0, 0.0, 1.0); + glColorMask(true, true, true, true); + state->haveAppliedAttribute(osg::StateAttribute::Type::COLORMASK); + glClear(GL_COLOR_BUFFER_BIT); + + bin->drawImplementation(renderInfo, previous); + + tex = mOriginalFBO[frameId]->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture(); + glViewport(0, 0, tex->getTextureWidth(), tex->getTextureHeight()); + mOriginalFBO[frameId]->apply(*state); + } +} diff --git a/apps/openmw/mwrender/distortion.hpp b/apps/openmw/mwrender/distortion.hpp new file mode 100644 index 0000000000..736f4ea6f2 --- /dev/null +++ b/apps/openmw/mwrender/distortion.hpp @@ -0,0 +1,28 @@ +#include + +#include + +namespace osg +{ + class FrameBufferObject; +} + +namespace MWRender +{ + class DistortionCallback : public osgUtil::RenderBin::DrawCallback + { + public: + void drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override; + + void setFBO(const osg::ref_ptr& fbo, size_t frameId) { mFBO[frameId] = fbo; } + void setOriginalFBO(const osg::ref_ptr& fbo, size_t frameId) + { + mOriginalFBO[frameId] = fbo; + } + + private: + std::array, 2> mFBO; + std::array, 2> mOriginalFBO; + }; +} diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index e5b8431c84..0ac509742c 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -27,7 +27,7 @@ namespace MWRender clear(); } - void EffectManager::addEffect(const std::string& model, std::string_view textureOverride, + void EffectManager::addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX) { osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model); @@ -52,9 +52,9 @@ namespace MWRender node->accept(assignVisitor); if (isMagicVFX) - overrideFirstRootTexture(textureOverride, mResourceSystem, node); + overrideFirstRootTexture(textureOverride, mResourceSystem, *node); else - overrideTexture(textureOverride, mResourceSystem, node); + overrideTexture(textureOverride, mResourceSystem, *node); mParentNode->addChild(trans); diff --git a/apps/openmw/mwrender/effectmanager.hpp b/apps/openmw/mwrender/effectmanager.hpp index 2477344fd0..671c441a59 100644 --- a/apps/openmw/mwrender/effectmanager.hpp +++ b/apps/openmw/mwrender/effectmanager.hpp @@ -2,11 +2,12 @@ #define OPENMW_MWRENDER_EFFECTMANAGER_H #include -#include #include #include +#include + namespace osg { class Group; @@ -33,8 +34,8 @@ namespace MWRender ~EffectManager(); /// Add an effect. When it's finished playing, it will be removed automatically. - void addEffect(const std::string& model, std::string_view textureOverride, const osg::Vec3f& worldPosition, - float scale, bool isMagicVFX = true); + void addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true); void update(float dt); diff --git a/apps/openmw/mwrender/esm4npcanimation.cpp b/apps/openmw/mwrender/esm4npcanimation.cpp new file mode 100644 index 0000000000..d0b54adb8f --- /dev/null +++ b/apps/openmw/mwrender/esm4npcanimation.cpp @@ -0,0 +1,189 @@ +#include "esm4npcanimation.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwclass/esm4npc.hpp" +#include "../mwworld/esmstore.hpp" + +namespace MWRender +{ + ESM4NpcAnimation::ESM4NpcAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) + : Animation(ptr, std::move(parentNode), resourceSystem) + { + setObjectRoot(mPtr.getClass().getCorrectedModel(mPtr), true, true, false); + updateParts(); + } + + void ESM4NpcAnimation::updateParts() + { + if (mObjectRoot == nullptr) + return; + const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); + if (traits == nullptr) + return; + if (traits->mIsTES4) + updatePartsTES4(*traits); + else if (traits->mIsFONV) + { + // Not implemented yet + } + else + { + // There is no easy way to distinguish TES5 and FO3. + // In case of FO3 the function shouldn't crash the game and will + // only lead to the NPC not being rendered. + updatePartsTES5(*traits); + } + } + + void ESM4NpcAnimation::insertPart(std::string_view model) + { + if (model.empty()) + return; + mResourceSystem->getSceneManager()->getInstance( + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(model)), mObjectRoot.get()); + } + + template + static std::string_view chooseTes4EquipmentModel(const Record* rec, bool isFemale) + { + if (isFemale && !rec->mModelFemale.empty()) + return rec->mModelFemale; + else if (!isFemale && !rec->mModelMale.empty()) + return rec->mModelMale; + else + return rec->mModel; + } + + void ESM4NpcAnimation::updatePartsTES4(const ESM4::Npc& traits) + { + const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); + bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); + + // TODO: Body and head parts are placed incorrectly, need to attach to bones + + for (const ESM4::Race::BodyPart& bodyPart : (isFemale ? race->mBodyPartsFemale : race->mBodyPartsMale)) + insertPart(bodyPart.mesh); + for (const ESM4::Race::BodyPart& bodyPart : race->mHeadParts) + insertPart(bodyPart.mesh); + if (!traits.mHair.isZeroOrUnset()) + { + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + if (const ESM4::Hair* hair = store->get().search(traits.mHair)) + insertPart(hair->mModel); + else + Log(Debug::Error) << "Hair not found: " << ESM::RefId(traits.mHair); + } + + for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) + insertPart(chooseTes4EquipmentModel(armor, isFemale)); + for (const ESM4::Clothing* clothing : MWClass::ESM4Npc::getEquippedClothing(mPtr)) + insertPart(chooseTes4EquipmentModel(clothing, isFemale)); + } + + void ESM4NpcAnimation::insertHeadParts( + const std::vector& partIds, std::set& usedHeadPartTypes) + { + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + for (ESM::FormId partId : partIds) + { + if (partId.isZeroOrUnset()) + continue; + const ESM4::HeadPart* part = store->get().search(partId); + if (!part) + { + Log(Debug::Error) << "Head part not found: " << ESM::RefId(partId); + continue; + } + if (usedHeadPartTypes.emplace(part->mType).second) + insertPart(part->mModel); + } + } + + void ESM4NpcAnimation::updatePartsTES5(const ESM4::Npc& traits) + { + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + + const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); + bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); + + std::vector armorAddons; + + auto findArmorAddons = [&](const ESM4::Armor* armor) { + for (ESM::FormId armaId : armor->mAddOns) + { + if (armaId.isZeroOrUnset()) + continue; + const ESM4::ArmorAddon* arma = store->get().search(armaId); + if (!arma) + { + Log(Debug::Error) << "ArmorAddon not found: " << ESM::RefId(armaId); + continue; + } + bool compatibleRace = arma->mRacePrimary == traits.mRace; + for (auto r : arma->mRaces) + if (r == traits.mRace) + compatibleRace = true; + if (compatibleRace) + armorAddons.push_back(arma); + } + }; + + for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) + findArmorAddons(armor); + if (!traits.mWornArmor.isZeroOrUnset()) + { + if (const ESM4::Armor* armor = store->get().search(traits.mWornArmor)) + findArmorAddons(armor); + else + Log(Debug::Error) << "Worn armor not found: " << ESM::RefId(traits.mWornArmor); + } + if (!race->mSkin.isZeroOrUnset()) + { + if (const ESM4::Armor* armor = store->get().search(race->mSkin)) + findArmorAddons(armor); + else + Log(Debug::Error) << "Skin not found: " << ESM::RefId(race->mSkin); + } + + if (isFemale) + std::sort(armorAddons.begin(), armorAddons.end(), + [](auto x, auto y) { return x->mFemalePriority > y->mFemalePriority; }); + else + std::sort(armorAddons.begin(), armorAddons.end(), + [](auto x, auto y) { return x->mMalePriority > y->mMalePriority; }); + + uint32_t usedParts = 0; + for (const ESM4::ArmorAddon* arma : armorAddons) + { + const uint32_t covers = arma->mBodyTemplate.bodyPart; + // if body is already covered, skip to avoid clipping + if (covers & usedParts & ESM4::Armor::TES5_Body) + continue; + // if covers at least something that wasn't covered before - add model + if (covers & ~usedParts) + { + usedParts |= covers; + insertPart(isFemale ? arma->mModelFemale : arma->mModelMale); + } + } + + std::set usedHeadPartTypes; + if (usedParts & ESM4::Armor::TES5_Hair) + usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair); + insertHeadParts(traits.mHeadParts, usedHeadPartTypes); + insertHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale, usedHeadPartTypes); + } +} diff --git a/apps/openmw/mwrender/esm4npcanimation.hpp b/apps/openmw/mwrender/esm4npcanimation.hpp new file mode 100644 index 0000000000..274c060b06 --- /dev/null +++ b/apps/openmw/mwrender/esm4npcanimation.hpp @@ -0,0 +1,31 @@ +#ifndef GAME_RENDER_ESM4NPCANIMATION_H +#define GAME_RENDER_ESM4NPCANIMATION_H + +#include "animation.hpp" + +namespace ESM4 +{ + struct Npc; +} + +namespace MWRender +{ + class ESM4NpcAnimation : public Animation + { + public: + ESM4NpcAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); + + private: + void insertPart(std::string_view model); + + // Works for FO3/FONV/TES5 + void insertHeadParts(const std::vector& partIds, std::set& usedHeadPartTypes); + + void updateParts(); + void updatePartsTES4(const ESM4::Npc& traits); + void updatePartsTES5(const ESM4::Npc& traits); + }; +} + +#endif // GAME_RENDER_ESM4NPCANIMATION_H diff --git a/apps/openmw/mwrender/fogmanager.cpp b/apps/openmw/mwrender/fogmanager.cpp index ef8de1cb2e..b75fb507ed 100644 --- a/apps/openmw/mwrender/fogmanager.cpp +++ b/apps/openmw/mwrender/fogmanager.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include "apps/openmw/mwworld/cell.hpp" namespace MWRender { diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index ac7a8a9351..f9dae65c40 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -363,7 +363,7 @@ namespace MWRender imageDest.mImage = image; imageDest.mX = x; imageDest.mY = y; - mPendingImageDest[camera] = imageDest; + mPendingImageDest[camera] = std::move(imageDest); } // Create a quad rendering the updated texture @@ -422,7 +422,8 @@ namespace MWRender if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; - requestOverlayTextureUpdate(originX, mHeight - originY, cellSize, cellSize, localMapTexture, false, true); + requestOverlayTextureUpdate( + originX, mHeight - originY, cellSize, cellSize, std::move(localMapTexture), false, true); } void GlobalMap::clear() @@ -554,7 +555,7 @@ namespace MWRender { mOverlayImage = image; - requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false); + requestOverlayTextureUpdate(0, 0, mWidth, mHeight, std::move(texture), true, false); } else { @@ -562,7 +563,7 @@ namespace MWRender // In the latter case, we'll want filtering. // Create a RTT Camera and draw the image onto mOverlayImage in the next frame. requestOverlayTextureUpdate(destBox.mLeft, destBox.mTop, destBox.mRight - destBox.mLeft, - destBox.mBottom - destBox.mTop, texture, true, true, srcBox.mLeft / float(imageWidth), + destBox.mBottom - destBox.mTop, std::move(texture), true, true, srcBox.mLeft / float(imageWidth), srcBox.mTop / float(imageHeight), srcBox.mRight / float(imageWidth), srcBox.mBottom / float(imageHeight)); } diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index e0582b20fa..07d7731e31 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -111,8 +111,6 @@ namespace MWRender ImageDestMap mPendingImageDest; - std::vector> mExploredCells; - osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index e00e3446a6..9fc40f3e19 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -1,5 +1,7 @@ #include "groundcover.hpp" +#include + #include #include #include @@ -14,6 +16,7 @@ #include #include #include +#include #include #include @@ -45,13 +48,13 @@ namespace MWRender class InstancedComputeNearFarCullCallback : public osg::DrawableCullCallback { public: - InstancedComputeNearFarCullCallback(const std::vector& instances, + explicit InstancedComputeNearFarCullCallback(std::span instances, const osg::Vec3& chunkPosition, const osg::BoundingBox& instanceBounds) : mInstanceMatrices() , mInstanceBounds(instanceBounds) { mInstanceMatrices.reserve(instances.size()); - for (const auto& instance : instances) + for (const Groundcover::GroundcoverEntry& instance : instances) mInstanceMatrices.emplace_back(computeInstanceMatrix(instance, chunkPosition)); } @@ -94,6 +97,8 @@ namespace MWRender { // Other objects are likely cheaper and should let us skip all but a few groundcover instances cullVisitor.computeNearPlane(); + computedZNear = cullVisitor.getCalculatedNearPlane(); + computedZFar = cullVisitor.getCalculatedFarPlane(); if (dNear < computedZNear) { @@ -188,13 +193,26 @@ namespace MWRender class InstancingVisitor : public osg::NodeVisitor { public: - InstancingVisitor(std::vector& instances, osg::Vec3f& chunkPosition) + explicit InstancingVisitor( + std::span instances, osg::Vec3f& chunkPosition) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mInstances(instances) , mChunkPosition(chunkPosition) { } + void apply(osg::Group& group) override + { + for (unsigned int i = 0; i < group.getNumChildren();) + { + if (group.getChild(i)->asDrawable() && !group.getChild(i)->asGeometry()) + group.removeChild(i); + else + ++i; + } + traverse(group); + } + void apply(osg::Geometry& geom) override { for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) @@ -237,7 +255,7 @@ namespace MWRender } private: - std::vector mInstances; + std::span mInstances; osg::Vec3f mChunkPosition; }; @@ -327,7 +345,7 @@ namespace MWRender Groundcover::Groundcover( Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store) - : GenericResourceManager(nullptr) + : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) , mSceneManager(sceneManager) , mDensity(density) , mStateset(new osg::StateSet) @@ -396,12 +414,15 @@ namespace MWRender } } - for (auto& pair : refs) + for (auto& [refNum, cellRef] : refs) { - ESM::CellRef& ref = pair.second; - const std::string& model = mGroundcoverStore.getGroundcoverModel(ref.mRefID); - if (!model.empty()) - instances[model].emplace_back(std::move(ref)); + const VFS::Path::NormalizedView model = mGroundcoverStore.getGroundcoverModel(cellRef.mRefID); + if (model.empty()) + continue; + auto it = instances.find(model); + if (it == instances.end()) + it = instances.emplace_hint(it, VFS::Path::Normalized(model), std::vector()); + it->second.emplace_back(std::move(cellRef)); } } } @@ -411,9 +432,9 @@ namespace MWRender { osg::ref_ptr group = new osg::Group; osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0) * ESM::Land::REAL_SIZE; - for (auto& pair : instances) + for (const auto& [model, entries] : instances) { - const osg::Node* temp = mSceneManager->getTemplate(pair.first); + const osg::Node* temp = mSceneManager->getTemplate(model); osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_NODES | osg::CopyOp::DEEP_COPY_DRAWABLES | osg::CopyOp::DEEP_COPY_USERDATA | osg::CopyOp::DEEP_COPY_ARRAYS | osg::CopyOp::DEEP_COPY_PRIMITIVES)); @@ -421,7 +442,7 @@ namespace MWRender // Keep link to original mesh to keep it in cache group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp)); - InstancingVisitor visitor(pair.second, worldCenter); + InstancingVisitor visitor(entries, worldCenter); node->accept(visitor); group->addChild(node); } @@ -448,6 +469,6 @@ namespace MWRender void Groundcover::reportStats(unsigned int frameNumber, osg::Stats* stats) const { - stats->setAttribute(frameNumber, "Groundcover Chunk", mCache->getCacheSize()); + Resource::reportStats("Groundcover Chunk", frameNumber, mCache->getStats(), *stats); } } diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp index df40d9d529..ecbc990da0 100644 --- a/apps/openmw/mwrender/groundcover.hpp +++ b/apps/openmw/mwrender/groundcover.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace MWWorld { @@ -46,13 +47,14 @@ namespace MWRender }; private: + using InstanceMap = std::map, std::less<>>; + Resource::SceneManager* mSceneManager; float mDensity; osg::ref_ptr mStateset; osg::ref_ptr mProgramTemplate; const MWWorld::GroundcoverStore& mGroundcoverStore; - typedef std::map> InstanceMap; osg::ref_ptr createChunk(InstanceMap& instances, const osg::Vec2f& center); void collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center); }; diff --git a/apps/openmw/mwrender/landmanager.cpp b/apps/openmw/mwrender/landmanager.cpp index 6ab9f12139..d17933b2b7 100644 --- a/apps/openmw/mwrender/landmanager.cpp +++ b/apps/openmw/mwrender/landmanager.cpp @@ -1,8 +1,8 @@ #include "landmanager.hpp" -#include - +#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -12,17 +12,24 @@ namespace MWRender { LandManager::LandManager(int loadFlags) - : GenericResourceManager(nullptr) + : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) , mLoadFlags(loadFlags) { } osg::ref_ptr LandManager::getLand(ESM::ExteriorCellLocation cellIndex) { + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + if (ESM::isEsm4Ext(cellIndex.mWorldspace)) + { + const ESM4::World* worldspace = world.getStore().get().find(cellIndex.mWorldspace); + if (!worldspace->mParent.isZeroOrUnset() && worldspace->mParentUseFlags & ESM4::World::UseFlag_Land) + cellIndex.mWorldspace = worldspace->mParent; + } + if (const std::optional> obj = mCache->getRefFromObjectCacheOrNone(cellIndex)) return static_cast(obj->get()); - const MWBase::World& world = *MWBase::Environment::get().getWorld(); osg::ref_ptr landObj = nullptr; if (ESM::isEsm4Ext(cellIndex.mWorldspace)) @@ -44,7 +51,7 @@ namespace MWRender void LandManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const { - stats->setAttribute(frameNumber, "Land", mCache->getCacheSize()); + Resource::reportStats("Land", frameNumber, mCache->getStats(), *stats); } } diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp index 7166c4b111..1b82f32ce9 100644 --- a/apps/openmw/mwrender/landmanager.hpp +++ b/apps/openmw/mwrender/landmanager.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include #include diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index a07fa10b09..f33ef35a52 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -30,6 +30,7 @@ #include "../mwworld/cellstore.hpp" +#include "util.hpp" #include "vismask.hpp" namespace @@ -136,6 +137,8 @@ namespace MWRender fog->mBounds.mMinY = mBounds.yMin(); fog->mBounds.mMaxY = mBounds.yMax(); fog->mNorthMarkerAngle = mAngle; + fog->mCenterX = mCenter.x(); + fog->mCenterY = mCenter.y(); fog->mFogTextures.reserve(segments.first * segments.second); @@ -144,15 +147,12 @@ namespace MWRender for (int y = 0; y < segments.second; ++y) { const MapSegment& segment = mInteriorSegments[std::make_pair(x, y)]; - - fog->mFogTextures.emplace_back(); - - // saving even if !segment.mHasFogState so we don't mess up the segmenting - // plus, older openmw versions can't deal with empty images - segment.saveFogOfWar(fog->mFogTextures.back()); - - fog->mFogTextures.back().mX = x; - fog->mFogTextures.back().mY = y; + if (!segment.mHasFogState) + continue; + ESM::FogTexture& texture = fog->mFogTextures.emplace_back(); + segment.saveFogOfWar(texture); + texture.mX = x; + texture.mY = y; } } @@ -331,22 +331,20 @@ namespace MWRender float zMin = mBounds.zMin(); float zMax = mBounds.zMax(); + mCenter = osg::Vec2f(mBounds.center().x(), mBounds.center().y()); // If there is fog state in the CellStore (e.g. when it came from a savegame) we need to do some checks // to see if this state is still valid. // Both the cell bounds and the NorthMarker rotation could be changed by the content files or exchanged models. // If they changed by too much then parts of the interior might not be covered by the map anymore. // The following code detects this, and discards the CellStore's fog state if it needs to. - std::vector> segmentMappings; - if (cell->getFog()) + int xOffset = 0; + int yOffset = 0; + if (const ESM::FogState* fog = cell->getFog()) { - ESM::FogState* fog = cell->getFog(); - if (std::abs(mAngle - fog->mNorthMarkerAngle) < osg::DegreesToRadians(5.f)) { // Expand mBounds so the saved textures fit the same grid - int xOffset = 0; - int yOffset = 0; if (fog->mBounds.mMinX < mBounds.xMin()) { mBounds.xMin() = fog->mBounds.mMinX; @@ -354,8 +352,7 @@ namespace MWRender else if (fog->mBounds.mMinX > mBounds.xMin()) { float diff = fog->mBounds.mMinX - mBounds.xMin(); - xOffset += diff / mMapWorldSize; - xOffset++; + xOffset = std::ceil(diff / mMapWorldSize); mBounds.xMin() = fog->mBounds.mMinX - xOffset * mMapWorldSize; } if (fog->mBounds.mMinY < mBounds.yMin()) @@ -365,8 +362,7 @@ namespace MWRender else if (fog->mBounds.mMinY > mBounds.yMin()) { float diff = fog->mBounds.mMinY - mBounds.yMin(); - yOffset += diff / mMapWorldSize; - yOffset++; + yOffset = std::ceil(diff / mMapWorldSize); mBounds.yMin() = fog->mBounds.mMinY - yOffset * mMapWorldSize; } if (fog->mBounds.mMaxX > mBounds.xMax()) @@ -377,22 +373,14 @@ namespace MWRender if (xOffset != 0 || yOffset != 0) Log(Debug::Warning) << "Warning: expanding fog by " << xOffset << ", " << yOffset; - const auto& textures = fog->mFogTextures; - segmentMappings.reserve(textures.size()); - osg::BoundingBox savedBounds{ fog->mBounds.mMinX, fog->mBounds.mMinY, 0, fog->mBounds.mMaxX, - fog->mBounds.mMaxY, 0 }; - auto segments = divideIntoSegments(savedBounds, mMapWorldSize); - for (int x = 0; x < segments.first; ++x) - for (int y = 0; y < segments.second; ++y) - segmentMappings.emplace_back(std::make_pair(x + xOffset, y + yOffset)); - mAngle = fog->mNorthMarkerAngle; + mCenter.x() = fog->mCenterX; + mCenter.y() = fog->mCenterY; } } osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); - osg::Vec2f center(mBounds.center().x(), mBounds.center().y()); osg::Quat cameraOrient(mAngle, osg::Vec3d(0, 0, -1)); auto segments = divideIntoSegments(mBounds, mMapWorldSize); @@ -403,10 +391,10 @@ namespace MWRender osg::Vec2f start = min + osg::Vec2f(mMapWorldSize * x, mMapWorldSize * y); osg::Vec2f newcenter = start + osg::Vec2f(mMapWorldSize / 2.f, mMapWorldSize / 2.f); - osg::Vec2f a = newcenter - center; + osg::Vec2f a = newcenter - mCenter; osg::Vec3f rotatedCenter = cameraOrient * (osg::Vec3f(a.x(), a.y(), 0)); - osg::Vec2f pos = osg::Vec2f(rotatedCenter.x(), rotatedCenter.y()) + center; + osg::Vec2f pos = osg::Vec2f(rotatedCenter.x(), rotatedCenter.y()) + mCenter; setupRenderToTexture(x, y, pos.x(), pos.y(), osg::Vec3f(north.x(), north.y(), 0.f), zMin, zMax); @@ -415,14 +403,16 @@ namespace MWRender if (!segment.mFogOfWarImage) { bool loaded = false; - for (size_t index{}; index < segmentMappings.size(); index++) + if (const ESM::FogState* fog = cell->getFog()) { - if (segmentMappings[index] == coords) + auto match = std::find_if( + fog->mFogTextures.begin(), fog->mFogTextures.end(), [&](const ESM::FogTexture& texture) { + return texture.mX == x - xOffset && texture.mY == y - yOffset; + }); + if (match != fog->mFogTextures.end()) { - ESM::FogState* fog = cell->getFog(); - segment.loadFogOfWar(fog->mFogTextures[index]); + segment.loadFogOfWar(*match); loaded = true; - break; } } if (!loaded) @@ -434,7 +424,7 @@ namespace MWRender void LocalMap::worldToInteriorMapPosition(osg::Vec2f pos, float& nX, float& nY, int& x, int& y) { - pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), mAngle); + pos = rotatePoint(pos, mCenter, mAngle); osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); @@ -450,7 +440,7 @@ namespace MWRender osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); osg::Vec2f pos(mMapWorldSize * (nX + x) + min.x(), mMapWorldSize * (1.0f - nY + y) + min.y()); - pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), -mAngle); + pos = rotatePoint(pos, mCenter, -mAngle); return pos; } @@ -586,6 +576,12 @@ namespace MWRender return result; } + MyGUI::IntRect LocalMap::getInteriorGrid() const + { + auto segments = divideIntoSegments(mBounds, mMapWorldSize); + return { -1, -1, segments.first, segments.second }; + } + void LocalMap::MapSegment::createFogOfWarTexture() { if (mFogOfWarTexture) @@ -679,7 +675,7 @@ namespace MWRender LocalMapRenderToTexture::LocalMapRenderToTexture(osg::Node* sceneRoot, int res, int mapWorldSize, float x, float y, const osg::Vec3d& upVector, float zmin, float zmax) - : RTTNode(res, res, 0, false, 0, StereoAwareness::Unaware_MultiViewShaders) + : RTTNode(res, res, 0, false, 0, StereoAwareness::Unaware_MultiViewShaders, shouldAddMSAAIntermediateTarget()) , mSceneRoot(sceneRoot) , mActive(true) { @@ -762,7 +758,7 @@ namespace MWRender lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*stateset); // override sun for local map SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot), light, stateset); diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index 9fd101c45f..3ba07ff2ee 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -98,6 +99,8 @@ namespace MWRender osg::Group* getRoot(); + MyGUI::IntRect getInteriorGrid() const; + private: osg::ref_ptr mRoot; osg::ref_ptr mSceneRoot; @@ -105,9 +108,6 @@ namespace MWRender typedef std::vector> RTTVector; RTTVector mLocalMapRTTs; - typedef std::set> Grid; - Grid mCurrentGrid; - enum NeighbourCellFlag : std::uint8_t { NeighbourCellTopLeft = 1, @@ -157,8 +157,9 @@ namespace MWRender void setupRenderToTexture( int segment_x, int segment_y, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax); - bool mInterior; osg::BoundingBox mBounds; + osg::Vec2f mCenter; + bool mInterior; std::uint8_t getExteriorNeighbourFlags(int cellX, int cellY) const; }; diff --git a/apps/openmw/mwrender/luminancecalculator.cpp b/apps/openmw/mwrender/luminancecalculator.cpp index 53240205c7..f974ea5ad7 100644 --- a/apps/openmw/mwrender/luminancecalculator.cpp +++ b/apps/openmw/mwrender/luminancecalculator.cpp @@ -1,7 +1,7 @@ #include "luminancecalculator.hpp" #include -#include +#include #include #include "pingpongcanvas.hpp" @@ -10,11 +10,8 @@ namespace MWRender { LuminanceCalculator::LuminanceCalculator(Shader::ShaderManager& shaderManager) { - const float hdrExposureTime - = std::max(Settings::Manager::getFloat("auto exposure speed", "Post Processing"), 0.0001f); - Shader::ShaderManager::DefineMap defines = { - { "hdrExposureTime", std::to_string(hdrExposureTime) }, + { "hdrExposureTime", std::to_string(Settings::postProcessing().mAutoExposureSpeed) }, }; auto vertex = shaderManager.getShader("fullscreen_tri.vert", {}); @@ -22,31 +19,23 @@ namespace MWRender auto resolveFragment = shaderManager.getShader("luminance/resolve.frag", defines); mResolveProgram = shaderManager.getProgram(vertex, std::move(resolveFragment)); - mLuminanceProgram = shaderManager.getProgram(vertex, std::move(luminanceFragment)); - } - - void LuminanceCalculator::compile() - { - int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); + mLuminanceProgram = shaderManager.getProgram(std::move(vertex), std::move(luminanceFragment)); for (auto& buffer : mBuffers) { buffer.mipmappedSceneLuminanceTex = new osg::Texture2D; buffer.mipmappedSceneLuminanceTex->setInternalFormat(GL_R16F); buffer.mipmappedSceneLuminanceTex->setSourceFormat(GL_RED); - buffer.mipmappedSceneLuminanceTex->setSourceType(GL_FLOAT); buffer.mipmappedSceneLuminanceTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); buffer.mipmappedSceneLuminanceTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); buffer.mipmappedSceneLuminanceTex->setFilter( osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR_MIPMAP_NEAREST); buffer.mipmappedSceneLuminanceTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR); buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); - buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); buffer.luminanceTex = new osg::Texture2D; buffer.luminanceTex->setInternalFormat(GL_R16F); buffer.luminanceTex->setSourceFormat(GL_RED); - buffer.luminanceTex->setSourceType(GL_FLOAT); buffer.luminanceTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); buffer.luminanceTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); buffer.luminanceTex->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::NEAREST); @@ -65,14 +54,6 @@ namespace MWRender buffer.luminanceProxyFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(buffer.luminanceProxyTex)); - buffer.resolveSceneLumFbo = new osg::FrameBufferObject; - buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, - osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); - - buffer.sceneLumFbo = new osg::FrameBufferObject; - buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, - osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); - buffer.sceneLumSS = new osg::StateSet; buffer.sceneLumSS->setAttributeAndModes(mLuminanceProgram); buffer.sceneLumSS->addUniform(new osg::Uniform("sceneTex", 0)); @@ -87,6 +68,26 @@ namespace MWRender mBuffers[0].resolveSS->setTextureAttributeAndModes(1, mBuffers[1].luminanceTex); mBuffers[1].resolveSS->setTextureAttributeAndModes(1, mBuffers[0].luminanceTex); + } + + void LuminanceCalculator::compile() + { + int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); + + for (auto& buffer : mBuffers) + { + buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); + buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); + buffer.mipmappedSceneLuminanceTex->dirtyTextureObject(); + + buffer.resolveSceneLumFbo = new osg::FrameBufferObject; + buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); + + buffer.sceneLumFbo = new osg::FrameBufferObject; + buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); + } mCompiled = true; } @@ -117,13 +118,14 @@ namespace MWRender buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); - if (dirty) + if (mIsBlank) { // Use current frame data for previous frame to warm up calculations and prevent popin mBuffers[(frameId + 1) % 2].resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + mIsBlank = false; } buffer.resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); diff --git a/apps/openmw/mwrender/luminancecalculator.hpp b/apps/openmw/mwrender/luminancecalculator.hpp index 71ea2f7971..8b51081e2f 100644 --- a/apps/openmw/mwrender/luminancecalculator.hpp +++ b/apps/openmw/mwrender/luminancecalculator.hpp @@ -58,6 +58,7 @@ namespace MWRender bool mCompiled = false; bool mEnabled = false; + bool mIsBlank = true; int mWidth = 1; int mHeight = 1; diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp index 8d638c918b..f1100ba502 100644 --- a/apps/openmw/mwrender/navmesh.cpp +++ b/apps/openmw/mwrender/navmesh.cpp @@ -1,4 +1,5 @@ #include "navmesh.hpp" + #include "vismask.hpp" #include @@ -6,10 +7,13 @@ #include #include #include +#include #include #include #include +#include +#include #include #include @@ -22,6 +26,29 @@ namespace MWRender { + namespace + { + osg::ref_ptr makeDebugDrawStateSet() + { + const osg::ref_ptr lineWidth = new osg::LineWidth(); + + const osg::ref_ptr blendFunc = new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + const osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateSet->setAttributeAndModes(lineWidth); + stateSet->setAttributeAndModes(blendFunc); + stateSet->setAttributeAndModes(depth); + + return stateSet; + } + } + struct NavMesh::LessByTilePosition { bool operator()(const DetourNavigator::TilePosition& lhs, @@ -46,7 +73,7 @@ namespace MWRender const osg::ref_ptr mDebugDrawStateSet; const DetourNavigator::Settings mSettings; std::map mTiles; - NavMeshMode mMode; + Settings::NavMeshRenderMode mMode; std::atomic_bool mAborted{ false }; std::mutex mMutex; bool mStarted = false; @@ -57,7 +84,7 @@ namespace MWRender std::weak_ptr navMesh, const osg::ref_ptr& groupStateSet, const osg::ref_ptr& debugDrawStateSet, const DetourNavigator::Settings& settings, const std::map& tiles, - NavMeshMode mode) + Settings::NavMeshRenderMode mode) : mId(id) , mVersion(version) , mNavMesh(std::move(navMesh)) @@ -110,13 +137,13 @@ namespace MWRender const unsigned char flags = SceneUtil::NavMeshTileDrawFlagsOffMeshConnections | SceneUtil::NavMeshTileDrawFlagsClosedList - | (mMode == NavMeshMode::UpdateFrequency ? SceneUtil::NavMeshTileDrawFlagsHeat : 0); + | (mMode == Settings::NavMeshRenderMode::UpdateFrequency ? SceneUtil::NavMeshTileDrawFlagsHeat : 0); for (const auto& [position, version] : existingTiles) { const auto it = mTiles.find(position); if (it != mTiles.end() && it->second.mGroup != nullptr && it->second.mVersion == version - && mMode != NavMeshMode::UpdateFrequency) + && mMode != Settings::NavMeshRenderMode::UpdateFrequency) continue; osg::ref_ptr group; @@ -129,16 +156,17 @@ namespace MWRender if (mAborted.load(std::memory_order_acquire)) return; - group = SceneUtil::createNavMeshTileGroup(navMesh->getImpl(), *meshTile, mSettings, mGroupStateSet, - mDebugDrawStateSet, flags, minSalt, maxSalt); + group = SceneUtil::createNavMeshTileGroup( + navMesh->getImpl(), *meshTile, mSettings, mDebugDrawStateSet, flags, minSalt, maxSalt); } if (group == nullptr) { removedTiles.push_back(position); continue; } - MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); group->setNodeMask(Mask_Debug); + group->setStateSet(mGroupStateSet); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); updatedTiles.emplace_back(position, Tile{ version, std::move(group) }); } @@ -163,11 +191,11 @@ namespace MWRender }; NavMesh::NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, - bool enabled, NavMeshMode mode) + bool enabled, Settings::NavMeshRenderMode mode) : mRootNode(root) , mWorkQueue(workQueue) - , mGroupStateSet(SceneUtil::makeNavMeshTileStateSet()) - , mDebugDrawStateSet(SceneUtil::DebugDraw::makeStateSet()) + , mGroupStateSet(SceneUtil::makeDetourGroupStateSet()) + , mDebugDrawStateSet(makeDebugDrawStateSet()) , mEnabled(enabled) , mMode(mode) , mId(std::numeric_limits::max()) @@ -310,7 +338,7 @@ namespace MWRender mEnabled = false; } - void NavMesh::setMode(NavMeshMode value) + void NavMesh::setMode(Settings::NavMeshRenderMode value) { if (mMode == value) return; diff --git a/apps/openmw/mwrender/navmesh.hpp b/apps/openmw/mwrender/navmesh.hpp index 4b4e50f791..8ac93e095f 100644 --- a/apps/openmw/mwrender/navmesh.hpp +++ b/apps/openmw/mwrender/navmesh.hpp @@ -1,18 +1,16 @@ #ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H -#include "navmeshmode.hpp" - #include #include #include +#include #include #include #include #include -#include #include class dtNavMesh; @@ -41,7 +39,7 @@ namespace MWRender { public: explicit NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, - bool enabled, NavMeshMode mode); + bool enabled, Settings::NavMeshRenderMode mode); ~NavMesh(); bool toggle(); @@ -57,7 +55,7 @@ namespace MWRender bool isEnabled() const { return mEnabled; } - void setMode(NavMeshMode value); + void setMode(Settings::NavMeshRenderMode value); private: struct Tile @@ -75,7 +73,7 @@ namespace MWRender osg::ref_ptr mGroupStateSet; osg::ref_ptr mDebugDrawStateSet; bool mEnabled; - NavMeshMode mMode; + Settings::NavMeshRenderMode mMode; std::size_t mId; DetourNavigator::Version mVersion; std::map mTiles; diff --git a/apps/openmw/mwrender/navmeshmode.cpp b/apps/openmw/mwrender/navmeshmode.cpp deleted file mode 100644 index d08e7cf693..0000000000 --- a/apps/openmw/mwrender/navmeshmode.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "navmeshmode.hpp" - -#include -#include - -namespace MWRender -{ - NavMeshMode parseNavMeshMode(std::string_view value) - { - if (value == "area type") - return NavMeshMode::AreaType; - if (value == "update frequency") - return NavMeshMode::UpdateFrequency; - throw std::logic_error("Unsupported navigation mesh rendering mode: " + std::string(value)); - } -} diff --git a/apps/openmw/mwrender/navmeshmode.hpp b/apps/openmw/mwrender/navmeshmode.hpp deleted file mode 100644 index 9401479e21..0000000000 --- a/apps/openmw/mwrender/navmeshmode.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef OPENMW_MWRENDER_NAVMESHMODE_H -#define OPENMW_MWRENDER_NAVMESHMODE_H - -#include - -namespace MWRender -{ - enum class NavMeshMode - { - AreaType, - UpdateFrequency, - }; - - NavMeshMode parseNavMeshMode(std::string_view value); -} - -#endif diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 6ddca9a674..c14eee27e4 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -40,6 +39,7 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" +#include "actorutil.hpp" #include "postprocessor.hpp" #include "renderbin.hpp" #include "rotatecontroller.hpp" @@ -48,7 +48,7 @@ namespace { - std::string getVampireHead(const ESM::RefId& race, bool female, const VFS::Manager& vfs) + std::string getVampireHead(const ESM::RefId& race, bool female) { static std::map, const ESM::BodyPart*> sVampireMapping; @@ -78,7 +78,7 @@ namespace const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; if (!bodyPart) return std::string(); - return Misc::ResourceHelpers::correctMeshPath(bodyPart->mModel, &vfs); + return Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bodyPart->mModel)).value(); } } @@ -155,6 +155,9 @@ namespace MWRender if (!mEnabled) return; + if (dt == 0.f) + return; + if (!MWBase::Environment::get().getSoundManager()->sayActive(mReference)) { mBlinkTimer += dt; @@ -312,9 +315,8 @@ namespace MWRender class DepthClearCallback : public osgUtil::RenderBin::DrawCallback { public: - DepthClearCallback(Resource::ResourceSystem* resourceSystem) + DepthClearCallback() { - mPassNormals = resourceSystem->getSceneManager()->getSupportsNormalsRT(); mDepth = new SceneUtil::AutoDepth; mDepth->setWriteMask(true); @@ -328,51 +330,33 @@ namespace MWRender { osg::State* state = renderInfo.getState(); - PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); + PostProcessor* postProcessor = static_cast(renderInfo.getCurrentCamera()->getUserData()); state->applyAttribute(mDepth); unsigned int frameId = state->getFrameStamp()->getFrameNumber() % 2; - if (postProcessor && postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)) - { - postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state); - if (mPassNormals) - { - state->get()->glColorMaski(1, true, true, true, true); - state->haveAppliedAttribute(osg::StateAttribute::COLORMASK); - } - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - // color accumulation pass - bin->drawImplementation(renderInfo, previous); + postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + // color accumulation pass + bin->drawImplementation(renderInfo, previous); - auto primaryFBO = postProcessor->getPrimaryFbo(frameId); + auto primaryFBO = postProcessor->getPrimaryFbo(frameId); + primaryFBO->apply(*state); - if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) - postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); - else - primaryFBO->apply(*state); + postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); - // depth accumulation pass - osg::ref_ptr restore = bin->getStateSet(); - bin->setStateSet(mStateSet); - bin->drawImplementation(renderInfo, previous); - bin->setStateSet(restore); + // depth accumulation pass + osg::ref_ptr restore = bin->getStateSet(); + bin->setStateSet(mStateSet); + bin->drawImplementation(renderInfo, previous); + bin->setStateSet(restore); - if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)) - primaryFBO->apply(*state); - } - else - { - // fallback to standard depth clear when we are not rendering our main scene via an intermediate FBO - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - bin->drawImplementation(renderInfo, previous); - } + primaryFBO->apply(*state); state->checkGLErrors("after DepthClearCallback::drawImplementation"); } - bool mPassNormals; osg::ref_ptr mDepth; osg::ref_ptr mStateSet; }; @@ -421,7 +405,7 @@ namespace MWRender if (!prototypeAdded) { osg::ref_ptr depthClearBin(new osgUtil::RenderBin); - depthClearBin->setDrawCallback(new DepthClearCallback(mResourceSystem)); + depthClearBin->setDrawCallback(new DepthClearCallback()); osgUtil::RenderBin::addRenderBinPrototype("DepthClear", depthClearBin); prototypeAdded = true; } @@ -472,14 +456,14 @@ namespace MWRender mHeadModel.clear(); mHairModel.clear(); - const ESM::RefId& headName = isWerewolf ? ESM::RefId::stringRefId("WerewolfHead") : mNpc->mHead; - const ESM::RefId& hairName = isWerewolf ? ESM::RefId::stringRefId("WerewolfHair") : mNpc->mHair; + const ESM::RefId headName = isWerewolf ? ESM::RefId::stringRefId("WerewolfHead") : mNpc->mHead; + const ESM::RefId hairName = isWerewolf ? ESM::RefId::stringRefId("WerewolfHair") : mNpc->mHair; if (!headName.empty()) { const ESM::BodyPart* bp = store.get().search(headName); if (bp) - mHeadModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS()); + mHeadModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bp->mModel)); else Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'"; } @@ -488,53 +472,59 @@ namespace MWRender { const ESM::BodyPart* bp = store.get().search(hairName); if (bp) - mHairModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS()); + mHairModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bp->mModel)); else Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'"; } - const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale, *mResourceSystem->getVFS()); + const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale); if (!isWerewolf && isVampire && !vampireHead.empty()) mHeadModel = vampireHead; bool is1stPerson = mViewMode == VM_FirstPerson; bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; - std::string defaultSkeleton = SceneUtil::getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf); - defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS()); + std::string_view base; + if (!isWerewolf) + { + if (!is1stPerson) + base = Settings::models().mXbaseanim.get().value(); + else + base = Settings::models().mXbaseanim1st.get().value(); + } + + const std::string defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( + VFS::Path::toNormalized(getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf)), + mResourceSystem->getVFS()); std::string smodel = defaultSkeleton; + bool isCustomModel = false; if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) - smodel = Misc::ResourceHelpers::correctActorModelPath( - Misc::ResourceHelpers::correctMeshPath(mNpc->mModel, mResourceSystem->getVFS()), - mResourceSystem->getVFS()); + { + VFS::Path::Normalized model = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(mNpc->mModel)); + isCustomModel = !isDefaultActorSkeleton(model); + smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); + } setObjectRoot(smodel, true, true, false); updateParts(); - if (!is1stPerson) - { - const std::string& base = Settings::Manager::getString("xbaseanim", "Models"); - if (smodel != base && !isWerewolf) - addAnimSource(base, smodel); + if (!base.empty()) + addAnimSource(base, smodel); - if (smodel != defaultSkeleton && base != defaultSkeleton) - addAnimSource(defaultSkeleton, smodel); + if (defaultSkeleton != base) + addAnimSource(defaultSkeleton, smodel); + if (isCustomModel) addAnimSource(smodel, smodel); - if (!isWerewolf && mNpc->mRace.contains("argonian")) - addAnimSource("meshes\\xargonian_swimkna.nif", smodel); - } - else + const bool customArgonianSwim = !is1stPerson && !isWerewolf && isBeast && mNpc->mRace.contains("argonian"); + if (customArgonianSwim) + addAnimSource(Settings::models().mXargonianswimkna.get().value(), smodel); + + if (is1stPerson) { - const std::string& base = Settings::Manager::getString("xbaseanim1st", "Models"); - if (smodel != base && !isWerewolf) - addAnimSource(base, smodel); - - addAnimSource(smodel, smodel); - mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); } @@ -549,8 +539,7 @@ namespace MWRender if (mesh.empty()) return std::string(); - std::string holsteredName = mesh; - holsteredName = holsteredName.replace(holsteredName.size() - 4, 4, "_sh.nif"); + const VFS::Path::Normalized holsteredName(addSuffixBeforeExtension(mesh, "_sh")); if (mResourceSystem->getVFS()->exists(holsteredName)) { osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); @@ -658,9 +647,8 @@ namespace MWRender if (store != inv.end() && (part = *store).getType() == ESM::Light::sRecordId) { const ESM::Light* light = part.get()->mBase; - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - Misc::ResourceHelpers::correctMeshPath(light->mModel, vfs), false, nullptr, true); + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(light->mModel)), false, nullptr, true); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), SceneUtil::LightCommon(*light)); } @@ -678,13 +666,9 @@ namespace MWRender { if (mPartPriorities[part] < 1) { - const ESM::BodyPart* bodypart = parts[part]; - if (bodypart) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + if (const ESM::BodyPart* bodypart = parts[part]) addOrReplaceIndividualPart(static_cast(part), -1, 1, - Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs)); - } + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bodypart->mModel))); } } @@ -692,14 +676,14 @@ namespace MWRender attachArrow(); } - PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, std::string_view bonename, + PartHolderPtr NpcAnimation::insertBoundedPart(VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); - return std::make_unique(attached); + return std::make_unique(std::move(attached)); } osg::Vec3f NpcAnimation::runAnimation(float timepassed) @@ -765,7 +749,7 @@ namespace MWRender } bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, - const std::string& mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) + VFS::Path::NormalizedView mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { if (priority <= mPartPriorities[type]) return false; @@ -852,7 +836,7 @@ namespace MWRender } } } - SceneUtil::ForceControllerSourcesVisitor assignVisitor(src); + SceneUtil::ForceControllerSourcesVisitor assignVisitor(std::move(src)); node->accept(assignVisitor); } else @@ -860,8 +844,8 @@ namespace MWRender if (type == ESM::PRT_Weapon) src = mWeaponAnimationTime; else - src = std::make_shared(); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); + src = mAnimationTimePtr[0]; + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::move(src)); node->accept(assignVisitor); } } @@ -913,11 +897,9 @@ namespace MWRender } if (bodypart) - { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); addOrReplaceIndividualPart(static_cast(part.mPart), group, priority, - Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs), enchantedGlow, glowColor); - } + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bodypart->mModel)), enchantedGlow, + glowColor); else reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); } @@ -932,13 +914,18 @@ namespace MWRender if (mViewMode == VM_FirstPerson) { - NodeMap::iterator found = mNodeMap.find("bip01 neck"); - if (found != mNodeMap.end()) + // If there is no active animation, then the bip01 neck node will not be updated each frame, and the + // RotateController will accumulate rotations. + if (mStates.size() > 0) { - osg::MatrixTransform* node = found->second.get(); - mFirstPersonNeckController = new RotateController(mObjectRoot.get()); - node->addUpdateCallback(mFirstPersonNeckController); - mActiveControllers.emplace_back(node, mFirstPersonNeckController); + NodeMap::iterator found = mNodeMap.find("bip01 neck"); + if (found != mNodeMap.end()) + { + osg::MatrixTransform* node = found->second.get(); + mFirstPersonNeckController = new RotateController(mObjectRoot.get()); + node->addUpdateCallback(mFirstPersonNeckController); + mActiveControllers.emplace_back(node, mFirstPersonNeckController); + } } } else if (mViewMode == VM_Normal) @@ -958,7 +945,7 @@ namespace MWRender if (weapon != inv.end()) { osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); - std::string mesh = weapon->getClass().getModel(*weapon); + const VFS::Path::Normalized mesh = weapon->getClass().getCorrectedModel(*weapon); addOrReplaceIndividualPart(ESM::PRT_Weapon, MWWorld::InventoryStore::Slot_CarriedRight, 1, mesh, !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor); @@ -1018,7 +1005,7 @@ namespace MWRender if (show && iter != inv.end()) { osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); - std::string mesh = iter->getClass().getModel(*iter); + VFS::Path::Normalized mesh = iter->getClass().getCorrectedModel(*iter); // For shields we must try to use the body part model if (iter->getType() == ESM::Armor::sRecordId) { diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index a03ee28f3a..1f9a656d65 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -1,13 +1,14 @@ #ifndef GAME_RENDER_NPCANIMATION_H #define GAME_RENDER_NPCANIMATION_H +#include "actoranimation.hpp" #include "animation.hpp" +#include "weaponanimation.hpp" + +#include #include "../mwworld/inventorystore.hpp" -#include "actoranimation.hpp" -#include "weaponanimation.hpp" - #include namespace ESM @@ -50,8 +51,8 @@ namespace MWRender std::array mSounds; const ESM::NPC* mNpc; - std::string mHeadModel; - std::string mHairModel; + VFS::Path::Normalized mHeadModel; + VFS::Path::Normalized mHairModel; ViewMode mViewMode; bool mShowWeapons; bool mShowCarriedLeft; @@ -83,14 +84,15 @@ namespace MWRender NpcType getNpcType() const; - PartHolderPtr insertBoundedPart(const std::string& model, std::string_view bonename, + PartHolderPtr insertBoundedPart(VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight); void removeIndividualPart(ESM::PartReferenceType type); void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); - bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string& mesh, - bool enchantedGlow = false, osg::Vec4f* glowColor = nullptr, bool isLight = false); + bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, + VFS::Path::NormalizedView mesh, bool enchantedGlow = false, osg::Vec4f* glowColor = nullptr, + bool isLight = false); void removePartGroup(int group); void addPartGroup(int group, int priority, const std::vector& parts, bool enchantedGlow = false, osg::Vec4f* glowColor = nullptr); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index c3c473de6b..f45247398f 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -9,31 +9,30 @@ #include #include #include +#include +#include #include #include #include +#include #include #include #include - #include +#include #include -#include -#include -#include -#include -#include - -#include -#include - #include +#include #include #include +#include +#include #include #include -#include +#include +#include +#include #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.hpp" @@ -41,81 +40,78 @@ #include "vismask.hpp" -#include - namespace MWRender { - - bool typeFilter(int type, bool far) + namespace { - switch (type) + bool typeFilter(int type, bool far) { - case ESM::REC_STAT: - case ESM::REC_ACTI: - case ESM::REC_DOOR: - return true; - case ESM::REC_CONT: - return !far; + switch (type) + { + case ESM::REC_STAT: + case ESM::REC_ACTI: + case ESM::REC_DOOR: + return true; + case ESM::REC_CONT: + return !far; - default: - return false; + default: + return false; + } + } + + std::string getModel(int type, ESM::RefId id, const MWWorld::ESMStore& store) + { + switch (type) + { + case ESM::REC_STAT: + return store.get().searchStatic(id)->mModel; + case ESM::REC_ACTI: + return store.get().searchStatic(id)->mModel; + case ESM::REC_DOOR: + return store.get().searchStatic(id)->mModel; + case ESM::REC_CONT: + return store.get().searchStatic(id)->mModel; + default: + return {}; + } } } - std::string getModel(int type, const ESM::RefId& id, const MWWorld::ESMStore& store) - { - switch (type) - { - case ESM::REC_STAT: - return store.get().searchStatic(id)->mModel; - case ESM::REC_ACTI: - return store.get().searchStatic(id)->mModel; - case ESM::REC_DOOR: - return store.get().searchStatic(id)->mModel; - case ESM::REC_CONT: - return store.get().searchStatic(id)->mModel; - default: - return {}; - } - } - - osg::ref_ptr ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char lod, + osg::ref_ptr ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char /*lod*/, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { - lod = static_cast(lodFlags >> (4 * 4)); if (activeGrid && !mActiveGrid) return nullptr; - ChunkId id = std::make_tuple(center, size, activeGrid); + const ChunkId id = std::make_tuple(center, size, activeGrid); - osg::ref_ptr obj = mCache->getRefFromObjectCache(id); - if (obj) + if (const osg::ref_ptr obj = mCache->getRefFromObjectCache(id)) return static_cast(obj.get()); - else - { - osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile, lod); - mCache->addEntryToObjectCache(id, node.get()); - return node; - } - } - class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback - { - public: - bool isOperationPermissibleForObjectImplementation( - const SceneUtil::Optimizer* optimizer, const osg::Drawable* node, unsigned int option) const override - { - return true; - } - bool isOperationPermissibleForObjectImplementation( - const SceneUtil::Optimizer* optimizer, const osg::Node* node, unsigned int option) const override - { - return (node->getDataVariance() != osg::Object::DYNAMIC); - } - }; + const unsigned char lod = static_cast(lodFlags >> (4 * 4)); + osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile, lod); + mCache->addEntryToObjectCache(id, node.get()); + return node; + } namespace { + class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback + { + public: + bool isOperationPermissibleForObjectImplementation( + const SceneUtil::Optimizer* optimizer, const osg::Drawable* node, unsigned int option) const override + { + return true; + } + bool isOperationPermissibleForObjectImplementation( + const SceneUtil::Optimizer* optimizer, const osg::Node* node, unsigned int option) const override + { + return (node->getDataVariance() != osg::Object::DYNAMIC); + } + }; + using LODRange = osg::LOD::MinMaxPair; LODRange intersection(const LODRange& left, const LODRange& right) @@ -132,447 +128,487 @@ namespace MWRender { return { r.first / div, r.second / div }; } - } - class CopyOp : public osg::CopyOp - { - public: - bool mOptimizeBillboards = true; - LODRange mDistances = { 0.f, 0.f }; - osg::Vec3f mViewVector; - osg::Node::NodeMask mCopyMask = ~0u; - mutable std::vector mNodePath; - - void copy(const osg::Node* toCopy, osg::Group* attachTo) + class CopyOp : public osg::CopyOp { - const osg::Group* groupToCopy = toCopy->asGroup(); - if (toCopy->getStateSet() || toCopy->asTransform() || !groupToCopy) - attachTo->addChild(operator()(toCopy)); - else + public: + bool mOptimizeBillboards = true; + bool mActiveGrid = false; + LODRange mDistances = { 0.f, 0.f }; + osg::Vec3f mViewVector; + osg::Node::NodeMask mCopyMask = ~0u; + mutable std::vector mNodePath; + + CopyOp(bool activeGrid, osg::Node::NodeMask copyMask) + : mActiveGrid(activeGrid) + , mCopyMask(copyMask) { - for (unsigned int i = 0; i < groupToCopy->getNumChildren(); ++i) - attachTo->addChild(operator()(groupToCopy->getChild(i))); } - } - osg::Node* operator()(const osg::Node* node) const override - { - if (!(node->getNodeMask() & mCopyMask)) - return nullptr; - - if (const osg::Drawable* d = node->asDrawable()) - return operator()(d); - - if (dynamic_cast(node)) - return nullptr; - if (dynamic_cast(node)) - return nullptr; - - if (const osg::Switch* sw = node->asSwitch()) + void copy(const osg::Node* toCopy, osg::Group* attachTo) { - osg::Group* n = new osg::Group; - for (unsigned int i = 0; i < sw->getNumChildren(); ++i) - if (sw->getValue(i)) - n->addChild(operator()(sw->getChild(i))); - n->setDataVariance(osg::Object::STATIC); - return n; - } - if (const osg::LOD* lod = dynamic_cast(node)) - { - std::vector, LODRange>> children; - for (unsigned int i = 0; i < lod->getNumChildren(); ++i) - if (const auto r = intersection(lod->getRangeList()[i], mDistances); !empty(r)) - children.emplace_back(operator()(lod->getChild(i)), lod->getRangeList()[i]); - if (children.empty()) - return nullptr; - - if (children.size() == 1) - return children.front().first.release(); + const osg::Group* groupToCopy = toCopy->asGroup(); + if (toCopy->getStateSet() || toCopy->asTransform() || !groupToCopy) + attachTo->addChild(operator()(toCopy)); else { - osg::LOD* n = new osg::LOD; - for (const auto& [child, range] : children) - n->addChild(child, range.first, range.second); + for (unsigned int i = 0; i < groupToCopy->getNumChildren(); ++i) + attachTo->addChild(operator()(groupToCopy->getChild(i))); + } + } + + osg::Node* operator()(const osg::Node* node) const override + { + if (!(node->getNodeMask() & mCopyMask)) + return nullptr; + + if (const osg::Drawable* d = node->asDrawable()) + return operator()(d); + + if (dynamic_cast(node)) + return nullptr; + if (dynamic_cast(node)) + return nullptr; + + if (const osg::Switch* sw = node->asSwitch()) + { + osg::Group* n = new osg::Group; + for (unsigned int i = 0; i < sw->getNumChildren(); ++i) + if (sw->getValue(i)) + n->addChild(operator()(sw->getChild(i))); n->setDataVariance(osg::Object::STATIC); return n; } - } - if (const osg::Sequence* sq = dynamic_cast(node)) - { - osg::Group* n = new osg::Group; - n->addChild(operator()(sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0))); - n->setDataVariance(osg::Object::STATIC); - return n; - } - - mNodePath.push_back(node); - - osg::Node* cloned = static_cast(node->clone(*this)); - cloned->setDataVariance(osg::Object::STATIC); - cloned->setUserDataContainer(nullptr); - cloned->setName(""); - - mNodePath.pop_back(); - - handleCallbacks(node, cloned); - - return cloned; - } - void handleCallbacks(const osg::Node* node, osg::Node* cloned) const - { - for (const osg::Callback* callback = node->getCullCallback(); callback != nullptr; - callback = callback->getNestedCallback()) - { - if (callback->className() == std::string("BillboardCallback")) + if (const osg::LOD* lod = dynamic_cast(node)) { - if (mOptimizeBillboards) + std::vector, LODRange>> children; + for (unsigned int i = 0; i < lod->getNumChildren(); ++i) + if (const auto r = intersection(lod->getRangeList()[i], mDistances); !empty(r)) + children.emplace_back(operator()(lod->getChild(i)), lod->getRangeList()[i]); + if (children.empty()) + return nullptr; + + if (children.size() == 1) + return children.front().first.release(); + else { - handleBillboard(cloned); - continue; + osg::LOD* n = new osg::LOD; + for (const auto& [child, range] : children) + n->addChild(child, range.first, range.second); + n->setDataVariance(osg::Object::STATIC); + return n; + } + } + if (const osg::Sequence* sq = dynamic_cast(node)) + { + osg::Group* n = new osg::Group; + n->addChild(operator()(sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0))); + n->setDataVariance(osg::Object::STATIC); + return n; + } + + mNodePath.push_back(node); + + osg::Node* cloned = static_cast(node->clone(*this)); + if (!mActiveGrid) + cloned->setDataVariance(osg::Object::STATIC); + cloned->setUserDataContainer(nullptr); + cloned->setName(""); + + mNodePath.pop_back(); + + handleCallbacks(node, cloned); + + return cloned; + } + void handleCallbacks(const osg::Node* node, osg::Node* cloned) const + { + for (const osg::Callback* callback = node->getCullCallback(); callback != nullptr; + callback = callback->getNestedCallback()) + { + if (callback->className() == std::string("BillboardCallback")) + { + if (mOptimizeBillboards) + { + handleBillboard(cloned); + continue; + } + else + cloned->setDataVariance(osg::Object::DYNAMIC); + } + + if (node->getCullCallback()->getNestedCallback()) + { + osg::Callback* clonedCallback = osg::clone(callback, osg::CopyOp::SHALLOW_COPY); + clonedCallback->setNestedCallback(nullptr); + cloned->addCullCallback(clonedCallback); } else - cloned->setDataVariance(osg::Object::DYNAMIC); + cloned->addCullCallback(const_cast(callback)); } + } + void handleBillboard(osg::Node* node) const + { + osg::Transform* transform = node->asTransform(); + if (!transform) + return; + osg::MatrixTransform* matrixTransform = transform->asMatrixTransform(); + if (!matrixTransform) + return; - if (node->getCullCallback()->getNestedCallback()) + osg::Matrix worldToLocal = osg::Matrix::identity(); + for (auto pathNode : mNodePath) + if (const osg::Transform* t = pathNode->asTransform()) + t->computeWorldToLocalMatrix(worldToLocal, nullptr); + worldToLocal = osg::Matrix::orthoNormal(worldToLocal); + + osg::Matrix billboardMatrix; + osg::Vec3f viewVector = -(mViewVector + worldToLocal.getTrans()); + viewVector.normalize(); + osg::Vec3f right = viewVector ^ osg::Vec3f(0, 0, 1); + right.normalize(); + osg::Vec3f up = right ^ viewVector; + up.normalize(); + billboardMatrix.makeLookAt(osg::Vec3f(0, 0, 0), viewVector, up); + billboardMatrix.invert(billboardMatrix); + + const osg::Matrix& oldMatrix = matrixTransform->getMatrix(); + float mag[3]; // attempt to preserve scale + for (int i = 0; i < 3; ++i) + mag[i] = std::sqrt(oldMatrix(0, i) * oldMatrix(0, i) + oldMatrix(1, i) * oldMatrix(1, i) + + oldMatrix(2, i) * oldMatrix(2, i)); + osg::Matrix newMatrix; + worldToLocal.setTrans(0, 0, 0); + newMatrix *= worldToLocal; + newMatrix.preMult(billboardMatrix); + newMatrix.preMultScale(osg::Vec3f(mag[0], mag[1], mag[2])); + newMatrix.setTrans(oldMatrix.getTrans()); + + matrixTransform->setMatrix(newMatrix); + } + osg::Drawable* operator()(const osg::Drawable* drawable) const override + { + if (!(drawable->getNodeMask() & mCopyMask)) + return nullptr; + + if (dynamic_cast(drawable)) + return nullptr; + + if (dynamic_cast(drawable)) + return nullptr; + if (const SceneUtil::RigGeometry* rig = dynamic_cast(drawable)) + return operator()(rig->getSourceGeometry()); + if (const SceneUtil::MorphGeometry* morph = dynamic_cast(drawable)) + return operator()(morph->getSourceGeometry()); + + if (getCopyFlags() & DEEP_COPY_DRAWABLES) { - osg::Callback* clonedCallback = osg::clone(callback, osg::CopyOp::SHALLOW_COPY); - clonedCallback->setNestedCallback(nullptr); - cloned->addCullCallback(clonedCallback); + osg::Drawable* d = static_cast(drawable->clone(*this)); + d->setDataVariance(osg::Object::STATIC); + d->setUserDataContainer(nullptr); + d->setName(""); + return d; } else - cloned->addCullCallback(const_cast(callback)); + return const_cast(drawable); } - } - void handleBillboard(osg::Node* node) const - { - osg::Transform* transform = node->asTransform(); - if (!transform) - return; - osg::MatrixTransform* matrixTransform = transform->asMatrixTransform(); - if (!matrixTransform) - return; - - osg::Matrix worldToLocal = osg::Matrix::identity(); - for (auto pathNode : mNodePath) - if (const osg::Transform* t = pathNode->asTransform()) - t->computeWorldToLocalMatrix(worldToLocal, nullptr); - worldToLocal = osg::Matrix::orthoNormal(worldToLocal); - - osg::Matrix billboardMatrix; - osg::Vec3f viewVector = -(mViewVector + worldToLocal.getTrans()); - viewVector.normalize(); - osg::Vec3f right = viewVector ^ osg::Vec3f(0, 0, 1); - right.normalize(); - osg::Vec3f up = right ^ viewVector; - up.normalize(); - billboardMatrix.makeLookAt(osg::Vec3f(0, 0, 0), viewVector, up); - billboardMatrix.invert(billboardMatrix); - - const osg::Matrix& oldMatrix = matrixTransform->getMatrix(); - float mag[3]; // attempt to preserve scale - for (int i = 0; i < 3; ++i) - mag[i] = std::sqrt(oldMatrix(0, i) * oldMatrix(0, i) + oldMatrix(1, i) * oldMatrix(1, i) - + oldMatrix(2, i) * oldMatrix(2, i)); - osg::Matrix newMatrix; - worldToLocal.setTrans(0, 0, 0); - newMatrix *= worldToLocal; - newMatrix.preMult(billboardMatrix); - newMatrix.preMultScale(osg::Vec3f(mag[0], mag[1], mag[2])); - newMatrix.setTrans(oldMatrix.getTrans()); - - matrixTransform->setMatrix(newMatrix); - } - osg::Drawable* operator()(const osg::Drawable* drawable) const override - { - if (!(drawable->getNodeMask() & mCopyMask)) - return nullptr; - - if (dynamic_cast(drawable)) - return nullptr; - - if (dynamic_cast(drawable)) - return nullptr; - if (const SceneUtil::RigGeometry* rig = dynamic_cast(drawable)) - return operator()(rig->getSourceGeometry()); - if (const SceneUtil::MorphGeometry* morph = dynamic_cast(drawable)) - return operator()(morph->getSourceGeometry()); - - if (getCopyFlags() & DEEP_COPY_DRAWABLES) - { - osg::Drawable* d = static_cast(drawable->clone(*this)); - d->setDataVariance(osg::Object::STATIC); - d->setUserDataContainer(nullptr); - d->setName(""); - return d; - } - else - return const_cast(drawable); - } - osg::Callback* operator()(const osg::Callback* callback) const override { return nullptr; } - }; - - class RefnumSet : public osg::Object - { - public: - RefnumSet() {} - RefnumSet(const RefnumSet& copy, const osg::CopyOp&) - : mRefnums(copy.mRefnums) - { - } - META_Object(MWRender, RefnumSet) - std::vector mRefnums; - }; - - class AnalyzeVisitor : public osg::NodeVisitor - { - public: - AnalyzeVisitor(osg::Node::NodeMask analyzeMask) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mCurrentStateSet(nullptr) - { - setTraversalMask(analyzeMask); - } - - typedef std::unordered_map StateSetCounter; - struct Result - { - StateSetCounter mStateSetCounter; - unsigned int mNumVerts = 0; + osg::Callback* operator()(const osg::Callback* callback) const override { return nullptr; } }; - void apply(osg::Node& node) override + class RefnumSet : public osg::Object { - if (node.getStateSet()) - mCurrentStateSet = node.getStateSet(); + public: + RefnumSet() {} + RefnumSet(const RefnumSet& copy, const osg::CopyOp&) + : mRefnums(copy.mRefnums) + { + } + META_Object(MWRender, RefnumSet) + std::vector mRefnums; + }; - if (osg::Switch* sw = node.asSwitch()) + class AnalyzeVisitor : public osg::NodeVisitor + { + public: + AnalyzeVisitor(osg::Node::NodeMask analyzeMask) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mCurrentStateSet(nullptr) { - for (unsigned int i = 0; i < sw->getNumChildren(); ++i) - if (sw->getValue(i)) - traverse(*sw->getChild(i)); - return; - } - if (osg::LOD* lod = dynamic_cast(&node)) - { - for (unsigned int i = 0; i < lod->getNumChildren(); ++i) - if (const auto r = intersection(lod->getRangeList()[i], mDistances); !empty(r)) - traverse(*lod->getChild(i)); - return; - } - if (osg::Sequence* sq = dynamic_cast(&node)) - { - traverse(*sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0)); - return; + setTraversalMask(analyzeMask); } - traverse(node); - } - void apply(osg::Geometry& geom) override - { - if (osg::Array* array = geom.getVertexArray()) - mResult.mNumVerts += array->getNumElements(); - - ++mResult.mStateSetCounter[mCurrentStateSet]; - ++mGlobalStateSetCounter[mCurrentStateSet]; - } - Result retrieveResult() - { - Result result = mResult; - mResult = Result(); - mCurrentStateSet = nullptr; - return result; - } - void addInstance(const Result& result) - { - for (auto pair : result.mStateSetCounter) - mGlobalStateSetCounter[pair.first] += pair.second; - } - float getMergeBenefit(const Result& result) - { - if (result.mStateSetCounter.empty()) - return 1; - float mergeBenefit = 0; - for (auto pair : result.mStateSetCounter) + typedef std::unordered_map StateSetCounter; + struct Result { - mergeBenefit += mGlobalStateSetCounter[pair.first]; + StateSetCounter mStateSetCounter; + unsigned int mNumVerts = 0; + }; + + void apply(osg::Node& node) override + { + if (node.getStateSet()) + mCurrentStateSet = node.getStateSet(); + + if (osg::Switch* sw = node.asSwitch()) + { + for (unsigned int i = 0; i < sw->getNumChildren(); ++i) + if (sw->getValue(i)) + traverse(*sw->getChild(i)); + return; + } + if (osg::LOD* lod = dynamic_cast(&node)) + { + for (unsigned int i = 0; i < lod->getNumChildren(); ++i) + if (const auto r = intersection(lod->getRangeList()[i], mDistances); !empty(r)) + traverse(*lod->getChild(i)); + return; + } + if (osg::Sequence* sq = dynamic_cast(&node)) + { + traverse(*sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0)); + return; + } + + traverse(node); } - mergeBenefit /= result.mStateSetCounter.size(); - return mergeBenefit; - } + void apply(osg::Geometry& geom) override + { + if (osg::Array* array = geom.getVertexArray()) + mResult.mNumVerts += array->getNumElements(); - Result mResult; - osg::StateSet* mCurrentStateSet; - StateSetCounter mGlobalStateSetCounter; - LODRange mDistances = { 0.f, 0.f }; - }; + ++mResult.mStateSetCounter[mCurrentStateSet]; + ++mGlobalStateSetCounter[mCurrentStateSet]; + } + Result retrieveResult() + { + Result result = mResult; + mResult = Result(); + mCurrentStateSet = nullptr; + return result; + } + void addInstance(const Result& result) + { + for (auto pair : result.mStateSetCounter) + mGlobalStateSetCounter[pair.first] += pair.second; + } + float getMergeBenefit(const Result& result) + { + if (result.mStateSetCounter.empty()) + return 1; + float mergeBenefit = 0; + for (auto pair : result.mStateSetCounter) + { + mergeBenefit += mGlobalStateSetCounter[pair.first]; + } + mergeBenefit /= result.mStateSetCounter.size(); + return mergeBenefit; + } - class DebugVisitor : public osg::NodeVisitor - { - public: - DebugVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - } - void apply(osg::Drawable& node) override - { - osg::ref_ptr m(new osg::Material); - osg::Vec4f color( - Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), 0.f); - color.normalize(); - m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f, 0.1f, 0.1f, 1.f)); - m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f, 0.1f, 0.1f, 1.f)); - m->setColorMode(osg::Material::OFF); - m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(color)); - osg::ref_ptr stateset - = node.getStateSet() ? osg::clone(node.getStateSet(), osg::CopyOp::SHALLOW_COPY) : new osg::StateSet; - stateset->setAttribute(m); - stateset->addUniform(new osg::Uniform("colorMode", 0)); - stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); - stateset->addUniform(new osg::Uniform("specStrength", 1.f)); - node.setStateSet(stateset); - } - }; + Result mResult; + osg::StateSet* mCurrentStateSet; + StateSetCounter mGlobalStateSetCounter; + LODRange mDistances = { 0.f, 0.f }; + }; - class AddRefnumMarkerVisitor : public osg::NodeVisitor - { - public: - AddRefnumMarkerVisitor(const ESM::RefNum& refnum) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mRefnum(refnum) + class DebugVisitor : public osg::NodeVisitor { - } - ESM::RefNum mRefnum; - void apply(osg::Geometry& node) override - { - osg::ref_ptr marker(new RefnumMarker); - marker->mRefnum = mRefnum; - if (osg::Array* array = node.getVertexArray()) - marker->mNumVertices = array->getNumElements(); - node.getOrCreateUserDataContainer()->addUserObject(marker); - } - }; + public: + DebugVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + { + } + void apply(osg::Drawable& node) override + { + osg::ref_ptr m(new osg::Material); + osg::Vec4f color( + Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), 0.f); + color.normalize(); + m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f, 0.1f, 0.1f, 1.f)); + m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f, 0.1f, 0.1f, 1.f)); + m->setColorMode(osg::Material::OFF); + m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(color)); + osg::ref_ptr stateset = node.getStateSet() + ? osg::clone(node.getStateSet(), osg::CopyOp::SHALLOW_COPY) + : new osg::StateSet; + stateset->setAttribute(m); + stateset->addUniform(new osg::Uniform("colorMode", 0)); + stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); + stateset->addUniform(new osg::Uniform("specStrength", 1.f)); + node.setStateSet(stateset); + } + }; - ObjectPaging::ObjectPaging(Resource::SceneManager* sceneManager, ESM::RefId worldspace) - : GenericResourceManager(nullptr) - , Terrain::QuadTreeWorld::ChunkManager(worldspace) - , mSceneManager(sceneManager) - , mRefTrackerLocked(false) - { - mActiveGrid = Settings::Manager::getBool("object paging active grid", "Terrain"); - mDebugBatches = Settings::Manager::getBool("debug chunks", "Terrain"); - mMergeFactor = Settings::Manager::getFloat("object paging merge factor", "Terrain"); - mMinSize = Settings::Manager::getFloat("object paging min size", "Terrain"); - mMinSizeMergeFactor = Settings::Manager::getFloat("object paging min size merge factor", "Terrain"); - mMinSizeCostMultiplier = Settings::Manager::getFloat("object paging min size cost multiplier", "Terrain"); + class AddRefnumMarkerVisitor : public osg::NodeVisitor + { + public: + AddRefnumMarkerVisitor(ESM::RefNum refnum) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mRefnum(refnum) + { + } + ESM::RefNum mRefnum; + void apply(osg::Geometry& node) override + { + osg::ref_ptr marker(new RefnumMarker); + marker->mRefnum = mRefnum; + if (osg::Array* array = node.getVertexArray()) + marker->mNumVertices = array->getNumElements(); + node.getOrCreateUserDataContainer()->addUserObject(marker); + } + }; } - std::map ObjectPaging::collectESM3References( - float size, const osg::Vec2i& startCell, ESM::ReadersCache& readers) const + ObjectPaging::ObjectPaging(Resource::SceneManager* sceneManager, ESM::RefId worldspace) + : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) + , Terrain::QuadTreeWorld::ChunkManager(worldspace) + , mSceneManager(sceneManager) + , mActiveGrid(Settings::terrain().mObjectPagingActiveGrid) + , mDebugBatches(Settings::terrain().mDebugChunks) + , mMergeFactor(Settings::terrain().mObjectPagingMergeFactor) + , mMinSize(Settings::terrain().mObjectPagingMinSize) + , mMinSizeMergeFactor(Settings::terrain().mObjectPagingMinSizeMergeFactor) + , mMinSizeCostMultiplier(Settings::terrain().mObjectPagingMinSizeCostMultiplier) + , mRefTrackerLocked(false) { - std::map refs; - const auto& store = MWBase::Environment::get().getWorld()->getStore(); - for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) + } + + namespace + { + struct PagedCellRef { - for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) + ESM::RefId mRefId; + ESM::RefNum mRefNum; + osg::Vec3f mPosition; + osg::Vec3f mRotation; + float mScale; + }; + + PagedCellRef makePagedCellRef(const ESM::CellRef& value) + { + return PagedCellRef{ + .mRefId = value.mRefID, + .mRefNum = value.mRefNum, + .mPosition = value.mPos.asVec3(), + .mRotation = value.mPos.asRotationVec3(), + .mScale = value.mScale, + }; + } + + std::map collectESM3References( + float size, const osg::Vec2i& startCell, const MWWorld::ESMStore& store) + { + std::map refs; + ESM::ReadersCache readers; + for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) { - const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); - if (!cell) - continue; - for (size_t i = 0; i < cell->mContextList.size(); ++i) + for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) { - try + const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); + if (!cell) + continue; + for (size_t i = 0; i < cell->mContextList.size(); ++i) { - const std::size_t index = static_cast(cell->mContextList[i].index); - const ESM::ReadersCache::BusyItem reader = readers.get(index); - cell->restore(*reader, i); - ESM::CellRef ref; - ESM::MovedCellRef cMRef; - bool deleted = false; - bool moved = false; - while (ESM::Cell::getNextRef( - *reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) + try { - if (moved) - continue; - - if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) - != cell->mMovedRefs.end()) - continue; - - int type = store.findStatic(ref.mRefID); - if (!typeFilter(type, size >= 2)) - continue; - if (deleted) + const std::size_t index = static_cast(cell->mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = readers.get(index); + cell->restore(*reader, i); + ESM::CellRef ref; + ESM::MovedCellRef cMRef; + bool deleted = false; + bool moved = false; + while (ESM::Cell::getNextRef( + *reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { - refs.erase(ref.mRefNum); - continue; + if (moved) + continue; + + if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) + != cell->mMovedRefs.end()) + continue; + + int type = store.findStatic(ref.mRefID); + if (!typeFilter(type, size >= 2)) + continue; + if (deleted) + { + refs.erase(ref.mRefNum); + continue; + } + refs.insert_or_assign(ref.mRefNum, makePagedCellRef(ref)); } - refs[ref.mRefNum] = std::move(ref); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to collect references from cell \"" << cell->getDescription() + << "\": " << e.what(); + continue; } } - catch (std::exception&) + for (const auto& [ref, deleted] : cell->mLeasedRefs) { - continue; + if (deleted) + { + refs.erase(ref.mRefNum); + continue; + } + int type = store.findStatic(ref.mRefID); + if (!typeFilter(type, size >= 2)) + continue; + refs.insert_or_assign(ref.mRefNum, makePagedCellRef(ref)); } } - for (auto [ref, deleted] : cell->mLeasedRefs) - { - if (deleted) - { - refs.erase(ref.mRefNum); - continue; - } - int type = store.findStatic(ref.mRefID); - if (!typeFilter(type, size >= 2)) - continue; - refs[ref.mRefNum] = std::move(ref); - } } + return refs; } - return refs; } osg::ref_ptr ObjectPaging::createChunk(float size, const osg::Vec2f& center, bool activeGrid, const osg::Vec3f& viewPoint, bool compile, unsigned char lod) { - osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size / 2.f), std::floor(center.y() - size / 2.f)); + const osg::Vec2i startCell(std::floor(center.x() - size / 2.f), std::floor(center.y() - size / 2.f)); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const MWWorld::ESMStore& store = world.getStore(); - osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0) * getCellSize(mWorldspace); - osg::Vec3f relativeViewPoint = viewPoint - worldCenter; - - std::map refs; - ESM::ReadersCache readers; - const auto& world = MWBase::Environment::get().getWorld(); - const auto& store = world->getStore(); + std::map refs; if (mWorldspace == ESM::Cell::sDefaultWorldspaceId) { - refs = collectESM3References(size, startCell, readers); + refs = collectESM3References(size, startCell, store); } else { // TODO } - if (activeGrid) + if (activeGrid && !refs.empty()) { std::lock_guard lock(mRefTrackerMutex); - for (auto ref : getRefTracker().mBlacklist) - refs.erase(ref); + const std::set& blacklist = getRefTracker().mBlacklist; + if (blacklist.size() < refs.size()) + { + for (ESM::RefNum ref : blacklist) + refs.erase(ref); + } + else + { + std::erase_if(refs, [&](const auto& ref) { return blacklist.contains(ref.first); }); + } } - osg::Vec2f minBound = (center - osg::Vec2f(size / 2.f, size / 2.f)); - osg::Vec2f maxBound = (center + osg::Vec2f(size / 2.f, size / 2.f)); + const osg::Vec2f minBound = (center - osg::Vec2f(size / 2.f, size / 2.f)); + const osg::Vec2f maxBound = (center + osg::Vec2f(size / 2.f, size / 2.f)); + const osg::Vec2i floorMinBound(std::floor(minBound.x()), std::floor(minBound.y())); + const osg::Vec2i ceilMaxBound(std::ceil(maxBound.x()), std::ceil(maxBound.y())); struct InstanceList { - std::vector mInstances; + std::vector mInstances; AnalyzeVisitor::Result mAnalyzeResult; bool mNeedCompile = false; }; typedef std::map, InstanceList> NodeMap; NodeMap nodes; - osg::ref_ptr refnumSet = activeGrid ? new RefnumSet : nullptr; + const osg::ref_ptr refnumSet = activeGrid ? new RefnumSet : nullptr; // Mask_UpdateVisitor is used in such cases in NIF loader: // 1. For collision nodes, which is not supposed to be rendered. @@ -580,58 +616,50 @@ namespace MWRender // Since ObjectPaging does not handle VisController, we can just ignore both types of nodes. constexpr auto copyMask = ~Mask_UpdateVisitor; - auto cellSize = getCellSize(mWorldspace); - const auto smallestDistanceToChunk = (size > 1 / 8.f) ? (size * cellSize) : 0.f; - const auto higherDistanceToChunk = [&] { - if (!activeGrid) - return smallestDistanceToChunk + 1; - return ((size < 1) ? 5 : 3) * cellSize * size + 1; - }(); + const int cellSize = getCellSize(mWorldspace); + const float smallestDistanceToChunk = (size > 1 / 8.f) ? (size * cellSize) : 0.f; + const float higherDistanceToChunk + = activeGrid ? ((size < 1) ? 5 : 3) * cellSize * size + 1 : smallestDistanceToChunk + 1; AnalyzeVisitor analyzeVisitor(copyMask); - float minSize = mMinSize; - if (mMinSizeMergeFactor) - minSize *= mMinSizeMergeFactor; - for (const auto& pair : refs) + const float minSize = mMinSizeMergeFactor ? mMinSize * mMinSizeMergeFactor : mMinSize; + for (const auto& [refNum, ref] : refs) { - const ESM::CellRef& ref = pair.second; - - osg::Vec3f pos = ref.mPos.asVec3(); if (size < 1.f) { - osg::Vec3f cellPos = pos / cellSize; - if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) - || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) - || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) - || (maxBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) + const osg::Vec3f cellPos = ref.mPosition / cellSize; + if ((minBound.x() > floorMinBound.x() && cellPos.x() < minBound.x()) + || (minBound.y() > floorMinBound.y() && cellPos.y() < minBound.y()) + || (maxBound.x() < ceilMaxBound.x() && cellPos.x() >= maxBound.x()) + || (maxBound.y() < ceilMaxBound.y() && cellPos.y() >= maxBound.y())) continue; } - float dSqr = (viewPoint - pos).length2(); + const float dSqr = (viewPoint - ref.mPosition).length2(); if (!activeGrid) { std::lock_guard lock(mSizeCacheMutex); - SizeCache::iterator found = mSizeCache.find(pair.first); + SizeCache::iterator found = mSizeCache.find(refNum); if (found != mSizeCache.end() && found->second < dSqr * minSize * minSize) continue; } - if (Misc::ResourceHelpers::isHiddenMarker(ref.mRefID)) + if (Misc::ResourceHelpers::isHiddenMarker(ref.mRefId)) continue; - int type = store.findStatic(ref.mRefID); - std::string model = getModel(type, ref.mRefID, store); + const int type = store.findStatic(ref.mRefId); + VFS::Path::Normalized model = getModel(type, ref.mRefId, store); if (model.empty()) continue; - model = Misc::ResourceHelpers::correctMeshPath(model, mSceneManager->getVFS()); + model = Misc::ResourceHelpers::correctMeshPath(model); if (activeGrid && type != ESM::REC_STAT) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); - std::string kfname = Misc::StringUtils::lowerCase(model); - if (kfname.size() > 4 && kfname.ends_with(".nif")) + if (Misc::getFileExtension(model) == "nif") { - kfname.replace(kfname.size() - 4, 4, ".kf"); + VFS::Path::Normalized kfname = model; + kfname.changeExtension("kf"); if (mSceneManager->getVFS()->exists(kfname)) continue; } @@ -646,11 +674,9 @@ namespace MWRender model = found->second; else model = mLODNameCache - .insert(found, - { key, - Misc::ResourceHelpers::getLODMeshName( - world->getESMVersions()[ref.mRefNum.mContentFile], model, - mSceneManager->getVFS(), lod) }) + .emplace_hint(found, std::move(key), + Misc::ResourceHelpers::getLODMeshName(world.getESMVersions()[refNum.mContentFile], + model, *mSceneManager->getVFS(), lod)) ->second; } @@ -665,76 +691,74 @@ namespace MWRender && dynamic_cast(cnode->getUpdateCallback()))) continue; else - refnumSet->mRefnums.push_back(pair.first); + refnumSet->mRefnums.push_back(refNum); } { std::lock_guard lock(mRefTrackerMutex); - if (getRefTracker().mDisabled.count(pair.first)) + if (getRefTracker().mDisabled.count(refNum)) continue; } - float radius2 = cnode->getBound().radius2() * ref.mScale * ref.mScale; + const float radius2 = cnode->getBound().radius2() * ref.mScale * ref.mScale; if (radius2 < dSqr * minSize * minSize && !activeGrid) { std::lock_guard lock(mSizeCacheMutex); - mSizeCache[pair.first] = radius2; + mSizeCache[refNum] = radius2; continue; } - auto emplaced = nodes.emplace(cnode, InstanceList()); + const auto emplaced = nodes.emplace(std::move(cnode), InstanceList()); if (emplaced.second) { analyzeVisitor.mDistances = LODRange{ smallestDistanceToChunk, higherDistanceToChunk } / ref.mScale; - const_cast(cnode.get()) - ->accept( - analyzeVisitor); // const-trickery required because there is no const version of NodeVisitor + const osg::Node* const nodePtr = emplaced.first->first.get(); + // const-trickery required because there is no const version of NodeVisitor + const_cast(nodePtr)->accept(analyzeVisitor); emplaced.first->second.mAnalyzeResult = analyzeVisitor.retrieveResult(); - emplaced.first->second.mNeedCompile = compile && cnode->referenceCount() <= 3; + emplaced.first->second.mNeedCompile = compile && nodePtr->referenceCount() <= 2; } else analyzeVisitor.addInstance(emplaced.first->second.mAnalyzeResult); emplaced.first->second.mInstances.push_back(&ref); } + const osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0) * getCellSize(mWorldspace); osg::ref_ptr group = new osg::Group; osg::ref_ptr mergeGroup = new osg::Group; osg::ref_ptr templateRefs = new Resource::TemplateMultiRef; osgUtil::StateToCompile stateToCompile(0, nullptr); - CopyOp copyop; - copyop.mCopyMask = copyMask; + CopyOp copyop(activeGrid, copyMask); for (const auto& pair : nodes) { const osg::Node* cnode = pair.first; const AnalyzeVisitor::Result& analyzeResult = pair.second.mAnalyzeResult; - float mergeCost = analyzeResult.mNumVerts * size; - float mergeBenefit = analyzeVisitor.getMergeBenefit(analyzeResult) * mMergeFactor; - bool merge = mergeBenefit > mergeCost; + const float mergeCost = analyzeResult.mNumVerts * size; + const float mergeBenefit = analyzeVisitor.getMergeBenefit(analyzeResult) * mMergeFactor; + const bool merge = mergeBenefit > mergeCost; - float minSizeMerged = mMinSize; - float factor2 = mergeBenefit > 0 ? std::min(1.f, mergeCost * mMinSizeCostMultiplier / mergeBenefit) : 1; - float minSizeMergeFactor2 = (1 - factor2) * mMinSizeMergeFactor + factor2; - if (minSizeMergeFactor2 > 0) - minSizeMerged *= minSizeMergeFactor2; + const float factor2 + = mergeBenefit > 0 ? std::min(1.f, mergeCost * mMinSizeCostMultiplier / mergeBenefit) : 1; + const float minSizeMergeFactor2 = (1 - factor2) * mMinSizeMergeFactor + factor2; + const float minSizeMerged = minSizeMergeFactor2 > 0 ? mMinSize * minSizeMergeFactor2 : mMinSize; unsigned int numinstances = 0; - for (auto cref : pair.second.mInstances) + for (const PagedCellRef* refPtr : pair.second.mInstances) { - const ESM::CellRef& ref = *cref; - osg::Vec3f pos = ref.mPos.asVec3(); + const PagedCellRef& ref = *refPtr; if (!activeGrid && minSizeMerged != minSize - && cnode->getBound().radius2() * cref->mScale * cref->mScale - < (viewPoint - pos).length2() * minSizeMerged * minSizeMerged) + && cnode->getBound().radius2() * ref.mScale * ref.mScale + < (viewPoint - ref.mPosition).length2() * minSizeMerged * minSizeMerged) continue; - osg::Vec3f nodePos = pos - worldCenter; - osg::Quat nodeAttitude = osg::Quat(ref.mPos.rot[2], osg::Vec3f(0, 0, -1)) - * osg::Quat(ref.mPos.rot[1], osg::Vec3f(0, -1, 0)) - * osg::Quat(ref.mPos.rot[0], osg::Vec3f(-1, 0, 0)); - osg::Vec3f nodeScale = osg::Vec3f(ref.mScale, ref.mScale, ref.mScale); + const osg::Vec3f nodePos = ref.mPosition - worldCenter; + const osg::Quat nodeAttitude = osg::Quat(ref.mRotation.z(), osg::Vec3f(0, 0, -1)) + * osg::Quat(ref.mRotation.y(), osg::Vec3f(0, -1, 0)) + * osg::Quat(ref.mRotation.x(), osg::Vec3f(-1, 0, 0)); + const osg::Vec3f nodeScale(ref.mScale, ref.mScale, ref.mScale); osg::ref_ptr trans; if (merge) @@ -787,7 +811,7 @@ namespace MWRender } } - osg::Group* attachTo = merge ? mergeGroup : group; + osg::Group* const attachTo = merge ? mergeGroup : group; attachTo->addChild(trans); ++numinstances; } @@ -808,6 +832,8 @@ namespace MWRender } } + const osg::Vec3f relativeViewPoint = viewPoint - worldCenter; + if (mergeGroup->getNumChildren()) { SceneUtil::Optimizer optimizer; @@ -817,7 +843,7 @@ namespace MWRender optimizer.setMergeAlphaBlending(true); } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); - unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS + const unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS | SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES | SceneUtil::Optimizer::MERGE_GEOMETRY; optimizer.optimize(mergeGroup, options); @@ -836,7 +862,7 @@ namespace MWRender } } - auto ico = mSceneManager->getIncrementalCompileOperation(); + osgUtil::IncrementalCompileOperation* const ico = mSceneManager->getIncrementalCompileOperation(); if (!stateToCompile.empty() && ico) { auto compileSet = new osgUtil::IncrementalCompileOperation::CompileSet(group); @@ -865,38 +891,51 @@ namespace MWRender return Mask_Static; } - struct ClearCacheFunctor + namespace { - void operator()(MWRender::ChunkId id, osg::Object* obj) + osg::Vec2f clampToCell(const osg::Vec3f& cellPos, const osg::Vec2i& cell) { - if (intersects(id, mPosition)) - mToClear.insert(id); + return osg::Vec2f(std::clamp(cellPos.x(), cell.x(), cell.x() + 1), + std::clamp(cellPos.y(), cell.y(), cell.y() + 1)); } - bool intersects(ChunkId id, osg::Vec3f pos) + + class CollectIntersecting { - if (mActiveGridOnly && !std::get<2>(id)) - return false; - pos /= getCellSize(mWorldspace); - clampToCell(pos); - osg::Vec2f center = std::get<0>(id); - float halfSize = std::get<1>(id) / 2; - return pos.x() >= center.x() - halfSize && pos.y() >= center.y() - halfSize - && pos.x() <= center.x() + halfSize && pos.y() <= center.y() + halfSize; - } - void clampToCell(osg::Vec3f& cellPos) - { - cellPos.x() = std::clamp(cellPos.x(), mCell.x(), mCell.x() + 1); - cellPos.y() = std::clamp(cellPos.y(), mCell.y(), mCell.y() + 1); - } - osg::Vec3f mPosition; - osg::Vec2i mCell; - ESM::RefId mWorldspace; - std::set mToClear; - bool mActiveGridOnly = false; - }; + public: + explicit CollectIntersecting( + bool activeGridOnly, const osg::Vec3f& position, const osg::Vec2i& cell, ESM::RefId worldspace) + : mActiveGridOnly(activeGridOnly) + , mPosition(clampToCell(position / getCellSize(worldspace), cell)) + { + } + + void operator()(const ChunkId& id, osg::Object* /*obj*/) + { + if (mActiveGridOnly && !std::get<2>(id)) + return; + if (intersects(id)) + mCollected.push_back(id); + } + + const std::vector& getCollected() const { return mCollected; } + + private: + bool intersects(ChunkId id) const + { + const osg::Vec2f center = std::get<0>(id); + const float halfSize = std::get<1>(id) / 2; + return mPosition.x() >= center.x() - halfSize && mPosition.y() >= center.y() - halfSize + && mPosition.x() <= center.x() + halfSize && mPosition.y() <= center.y() + halfSize; + } + + bool mActiveGridOnly; + osg::Vec2f mPosition; + std::vector mCollected; + }; + } bool ObjectPaging::enableObject( - int type, const ESM::RefNum& refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled) + int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled) { if (!typeFilter(type, false)) return false; @@ -911,20 +950,16 @@ namespace MWRender return false; } - ClearCacheFunctor ccf; - ccf.mPosition = pos; - ccf.mCell = cell; - ccf.mWorldspace = mWorldspace; + CollectIntersecting ccf(false, pos, cell, mWorldspace); mCache->call(ccf); - if (ccf.mToClear.empty()) + if (ccf.getCollected().empty()) return false; - for (const auto& chunk : ccf.mToClear) + for (const ChunkId& chunk : ccf.getCollected()) mCache->removeFromObjectCache(chunk); return true; } - bool ObjectPaging::blacklistObject( - int type, const ESM::RefNum& refnum, const osg::Vec3f& pos, const osg::Vec2i& cell) + bool ObjectPaging::blacklistObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell) { if (!typeFilter(type, false)) return false; @@ -937,15 +972,11 @@ namespace MWRender return false; } - ClearCacheFunctor ccf; - ccf.mPosition = pos; - ccf.mCell = cell; - ccf.mActiveGridOnly = true; - ccf.mWorldspace = mWorldspace; + CollectIntersecting ccf(true, pos, cell, mWorldspace); mCache->call(ccf); - if (ccf.mToClear.empty()) + if (ccf.getCollected().empty()) return false; - for (const auto& chunk : ccf.mToClear) + for (const ChunkId& chunk : ccf.getCollected()) mCache->removeFromObjectCache(chunk); return true; } @@ -974,34 +1005,37 @@ namespace MWRender return true; } - struct GetRefnumsFunctor + namespace { - GetRefnumsFunctor(std::vector& output) - : mOutput(output) + struct GetRefnumsFunctor { - } - void operator()(MWRender::ChunkId chunkId, osg::Object* obj) - { - if (!std::get<2>(chunkId)) - return; - const osg::Vec2f& center = std::get<0>(chunkId); - bool activeGrid = (center.x() > mActiveGrid.x() || center.y() > mActiveGrid.y() - || center.x() < mActiveGrid.z() || center.y() < mActiveGrid.w()); - if (!activeGrid) - return; - - osg::UserDataContainer* udc = obj->getUserDataContainer(); - if (udc && udc->getNumUserObjects()) + GetRefnumsFunctor(std::vector& output) + : mOutput(output) { - RefnumSet* refnums = dynamic_cast(udc->getUserObject(0)); - if (!refnums) - return; - mOutput.insert(mOutput.end(), refnums->mRefnums.begin(), refnums->mRefnums.end()); } - } - osg::Vec4i mActiveGrid; - std::vector& mOutput; - }; + void operator()(MWRender::ChunkId chunkId, osg::Object* obj) + { + if (!std::get<2>(chunkId)) + return; + const osg::Vec2f& center = std::get<0>(chunkId); + const bool activeGrid = (center.x() > mActiveGrid.x() || center.y() > mActiveGrid.y() + || center.x() < mActiveGrid.z() || center.y() < mActiveGrid.w()); + if (!activeGrid) + return; + + osg::UserDataContainer* udc = obj->getUserDataContainer(); + if (udc && udc->getNumUserObjects()) + { + RefnumSet* refnums = dynamic_cast(udc->getUserObject(0)); + if (!refnums) + return; + mOutput.insert(mOutput.end(), refnums->mRefnums.begin(), refnums->mRefnums.end()); + } + } + osg::Vec4i mActiveGrid; + std::vector& mOutput; + }; + } void ObjectPaging::getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out) { @@ -1014,7 +1048,7 @@ namespace MWRender void ObjectPaging::reportStats(unsigned int frameNumber, osg::Stats* stats) const { - stats->setAttribute(frameNumber, "Object Chunk", mCache->getCacheSize()); + Resource::reportStats("Object Chunk", frameNumber, mCache->getStats(), *stats); } } diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp index d677c172f2..6359aa9b0c 100644 --- a/apps/openmw/mwrender/objectpaging.hpp +++ b/apps/openmw/mwrender/objectpaging.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWRENDER_OBJECTPAGING_H #define OPENMW_MWRENDER_OBJECTPAGING_H -#include +#include #include #include @@ -11,15 +11,6 @@ namespace Resource { class SceneManager; } -namespace MWWorld -{ - class ESMStore; -} - -namespace ESM -{ - class ReadersCache; -} namespace MWRender { @@ -41,11 +32,10 @@ namespace MWRender unsigned int getNodeMask() override; /// @return true if view needs rebuild - bool enableObject( - int type, const ESM::RefNum& refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled); + bool enableObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled); /// @return true if view needs rebuild - bool blacklistObject(int type, const ESM::RefNum& refnum, const osg::Vec3f& pos, const osg::Vec2i& cell); + bool blacklistObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell); void clear(); @@ -83,16 +73,13 @@ namespace MWRender const RefTracker& getRefTracker() const { return mRefTracker; } RefTracker& getWritableRefTracker() { return mRefTrackerLocked ? mRefTrackerNew : mRefTracker; } - std::map collectESM3References( - float size, const osg::Vec2i& startCell, ESM::ReadersCache& readers) const; - std::mutex mSizeCacheMutex; typedef std::map SizeCache; SizeCache mSizeCache; std::mutex mLODNameCacheMutex; typedef std::pair LODNameCacheKey; // Key: mesh name, lod level - typedef std::map LODNameCache; // Cache: key, mesh name to use + using LODNameCache = std::map; // Cache: key, mesh name to use LODNameCache mLODNameCache; }; diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index b8b7d62309..9393846d7a 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -13,6 +13,7 @@ #include "animation.hpp" #include "creatureanimation.hpp" +#include "esm4npcanimation.hpp" #include "npcanimation.hpp" #include "vismask.hpp" @@ -67,7 +68,7 @@ namespace MWRender ptr.getClass().adjustScale(ptr, scaleVec, true); insert->setScale(scaleVec); - ptr.getRefData().setBaseNode(insert); + ptr.getRefData().setBaseNode(std::move(insert)); } void Objects::insertModel(const MWWorld::Ptr& ptr, const std::string& mesh, bool allowLight) @@ -78,7 +79,8 @@ namespace MWRender std::string animationMesh = mesh; if (animated && !mesh.empty()) { - animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + animationMesh = Misc::ResourceHelpers::correctActorModelPath( + VFS::Path::toNormalized(mesh), mResourceSystem->getVFS()); if (animationMesh == mesh && Misc::StringUtils::ciEndsWith(animationMesh, ".nif")) animated = false; } @@ -95,7 +97,8 @@ namespace MWRender ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); bool animated = true; - std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + std::string animationMesh + = Misc::ResourceHelpers::correctActorModelPath(VFS::Path::toNormalized(mesh), mResourceSystem->getVFS()); if (animationMesh == mesh && Misc::StringUtils::ciEndsWith(animationMesh, ".nif")) animated = false; @@ -116,13 +119,22 @@ namespace MWRender insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); - osg::ref_ptr anim( - new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); - - if (mObjects.emplace(ptr.mRef, anim).second) + if (ptr.getType() == ESM::REC_NPC_4) { - ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); - ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); + osg::ref_ptr anim( + new ESM4NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); + mObjects.emplace(ptr.mRef, anim); + } + else + { + osg::ref_ptr anim( + new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); + + if (mObjects.emplace(ptr.mRef, anim).second) + { + ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); + ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); + } } } diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp index 6a56f7e5f7..54d8145fa9 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -1,5 +1,7 @@ #include "pingpongcanvas.hpp" +#include + #include #include #include @@ -10,9 +12,11 @@ namespace MWRender { - PingPongCanvas::PingPongCanvas(Shader::ShaderManager& shaderManager) + PingPongCanvas::PingPongCanvas( + Shader::ShaderManager& shaderManager, const std::shared_ptr& luminanceCalculator) : mFallbackStateSet(new osg::StateSet) , mMultiviewResolveStateSet(new osg::StateSet) + , mLuminanceCalculator(luminanceCalculator) { setUseDisplayList(false); setUseVertexBufferObjects(true); @@ -26,8 +30,7 @@ namespace MWRender addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3)); - mLuminanceCalculator = LuminanceCalculator(shaderManager); - mLuminanceCalculator.disable(); + mLuminanceCalculator->disable(); Shader::ShaderManager::DefineMap defines; Stereo::shaderStereoDefines(defines); @@ -43,19 +46,16 @@ namespace MWRender mMultiviewResolveStateSet->addUniform(new osg::Uniform("lastShader", 0)); } - void PingPongCanvas::setCurrentFrameData(size_t frameId, fx::DispatchArray&& data) + void PingPongCanvas::setPasses(fx::DispatchArray&& passes) { - mBufferData[frameId].data = std::move(data); + mPasses = std::move(passes); } - void PingPongCanvas::setMask(size_t frameId, bool underwater, bool exterior) + void PingPongCanvas::setMask(bool underwater, bool exterior) { - mBufferData[frameId].mask = 0; - - mBufferData[frameId].mask - |= underwater ? fx::Technique::Flag_Disable_Underwater : fx::Technique::Flag_Disable_Abovewater; - mBufferData[frameId].mask - |= exterior ? fx::Technique::Flag_Disable_Exteriors : fx::Technique::Flag_Disable_Interiors; + mMask = 0; + mMask |= underwater ? fx::Technique::Flag_Disable_Underwater : fx::Technique::Flag_Disable_Abovewater; + mMask |= exterior ? fx::Technique::Flag_Disable_Exteriors : fx::Technique::Flag_Disable_Interiors; } void PingPongCanvas::drawGeometry(osg::RenderInfo& renderInfo) const @@ -77,19 +77,15 @@ namespace MWRender size_t frameId = state.getFrameStamp()->getFrameNumber() % 2; - auto& bufferData = mBufferData[frameId]; - - const auto& data = bufferData.data; - std::vector filtered; - filtered.reserve(data.size()); + filtered.reserve(mPasses.size()); - for (size_t i = 0; i < data.size(); ++i) + for (size_t i = 0; i < mPasses.size(); ++i) { - const auto& node = data[i]; + const auto& node = mPasses[i]; - if (bufferData.mask & node.mFlags) + if (mMask & node.mFlags) continue; filtered.push_back(i); @@ -97,7 +93,7 @@ namespace MWRender auto* resolveViewport = state.getCurrentViewport(); - if (filtered.empty() || !bufferData.postprocessing) + if (filtered.empty() || !mPostprocessing) { state.pushStateSet(mFallbackStateSet); state.apply(); @@ -108,7 +104,7 @@ namespace MWRender state.apply(); } - state.applyTextureAttribute(0, bufferData.sceneTex); + state.applyTextureAttribute(0, mTextureScene); resolveViewport->apply(state); drawGeometry(renderInfo); @@ -124,13 +120,12 @@ namespace MWRender const unsigned int handle = mFbos[0] ? mFbos[0]->getHandle(state.getContextID()) : 0; - if (handle == 0 || bufferData.dirty) + if (handle == 0 || mDirty) { for (auto& fbo : mFbos) { fbo = new osg::FrameBufferObject; - attachCloneOfTemplate( - fbo, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, bufferData.sceneTexLDR); + attachCloneOfTemplate(fbo, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, mTextureScene); fbo->apply(state); glClearColor(0.5, 0.5, 0.5, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -140,7 +135,7 @@ namespace MWRender { mMultiviewResolveFramebuffer = new osg::FrameBufferObject(); attachCloneOfTemplate(mMultiviewResolveFramebuffer, - osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, bufferData.sceneTexLDR); + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, mTextureScene); mMultiviewResolveFramebuffer->apply(state); glClearColor(0.5, 0.5, 0.5, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -150,15 +145,15 @@ namespace MWRender .getTexture()); } - mLuminanceCalculator.dirty(bufferData.sceneTex->getTextureWidth(), bufferData.sceneTex->getTextureHeight()); + mLuminanceCalculator->dirty(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); if (Stereo::getStereo()) - mRenderViewport = new osg::Viewport( - 0, 0, bufferData.sceneTex->getTextureWidth(), bufferData.sceneTex->getTextureHeight()); + mRenderViewport + = new osg::Viewport(0, 0, mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); else mRenderViewport = nullptr; - bufferData.dirty = false; + mDirty = false; } constexpr std::array, 3> buffers @@ -166,11 +161,11 @@ namespace MWRender { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT2_EXT }, { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT } } }; - (bufferData.hdr) ? mLuminanceCalculator.enable() : mLuminanceCalculator.disable(); + (mAvgLum) ? mLuminanceCalculator->enable() : mLuminanceCalculator->disable(); // A histogram based approach is superior way to calculate scene luminance. Using mipmaps is more broadly // supported, so that's what we use for now. - mLuminanceCalculator.draw(*this, renderInfo, state, ext, frameId); + mLuminanceCalculator->draw(*this, renderInfo, state, ext, frameId); auto buffer = buffers[0]; @@ -181,8 +176,7 @@ namespace MWRender const unsigned int cid = state.getContextID(); - const osg::ref_ptr& destinationFbo - = bufferData.destination ? bufferData.destination : nullptr; + const osg::ref_ptr& destinationFbo = mDestinationFBO ? mDestinationFBO : nullptr; unsigned int destinationHandle = destinationFbo ? destinationFbo->getHandle(cid) : 0; auto bindDestinationFbo = [&]() { @@ -204,19 +198,55 @@ namespace MWRender } }; + // When textures are created (or resized) we need to either dirty them and/or clear them. + // Otherwise, there will be undefined behavior when reading from a texture that has yet to be written to in a + // later pass. + for (const auto& attachment : mDirtyAttachments) + { + const auto [w, h] + = attachment.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + + attachment.mTarget->setTextureSize(w, h); + if (attachment.mMipMap) + attachment.mTarget->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + attachment.mTarget->dirtyTextureObject(); + + osg::ref_ptr fbo = new osg::FrameBufferObject; + + fbo->setAttachment( + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(attachment.mTarget)); + fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + glViewport(0, 0, attachment.mTarget->getTextureWidth(), attachment.mTarget->getTextureHeight()); + state.haveAppliedAttribute(osg::StateAttribute::VIEWPORT); + glClearColor(attachment.mClearColor.r(), attachment.mClearColor.g(), attachment.mClearColor.b(), + attachment.mClearColor.a()); + glClear(GL_COLOR_BUFFER_BIT); + + if (attachment.mTarget->getNumMipmapLevels() > 0) + { + state.setActiveTextureUnit(0); + state.applyTextureAttribute(0, attachment.mTarget); + ext->glGenerateMipmap(GL_TEXTURE_2D); + } + } + for (const size_t& index : filtered) { - const auto& node = data[index]; + const auto& node = mPasses[index]; - node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, bufferData.depthTex); + node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, mTextureDepth); - if (bufferData.hdr) + if (mAvgLum) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_EyeAdaptation, + mLuminanceCalculator->getLuminanceTexture(frameId)); + + if (mTextureNormals) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_Normals, mTextureNormals); + + if (mTextureDistortion) node.mRootStateSet->setTextureAttribute( - PostProcessor::TextureUnits::Unit_EyeAdaptation, mLuminanceCalculator.getLuminanceTexture(frameId)); - - if (bufferData.normalsTex) - node.mRootStateSet->setTextureAttribute( - PostProcessor::TextureUnits::Unit_Normals, bufferData.normalsTex); + PostProcessor::TextureUnits::Unit_Distortion, mTextureDistortion); state.pushStateSet(node.mRootStateSet); state.apply(); @@ -231,7 +261,7 @@ namespace MWRender // VR-TODO: This won't actually work for tex2darrays if (lastShader == 0) - pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, bufferData.sceneTex); + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, mTextureScene); else pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, (osg::Texture*)mFbos[lastShader - GL_COLOR_ATTACHMENT0_EXT] @@ -239,7 +269,7 @@ namespace MWRender .getTexture()); if (lastDraw == 0) - pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, bufferData.sceneTex); + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, mTextureScene); else pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, (osg::Texture*)mFbos[lastDraw - GL_COLOR_ATTACHMENT0_EXT] @@ -260,7 +290,6 @@ namespace MWRender } lastApplied = pass.mRenderTarget->getHandle(state.getContextID()); - ; } else if (pass.mResolve && index == filtered.back()) { @@ -322,5 +351,7 @@ namespace MWRender { bindDestinationFbo(); } + + mDirtyAttachments.clear(); } } diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp index a5557a6d6e..5a37b7fbc9 100644 --- a/apps/openmw/mwrender/pingpongcanvas.hpp +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -22,78 +22,64 @@ namespace MWRender class PingPongCanvas : public osg::Geometry { public: - PingPongCanvas(Shader::ShaderManager& shaderManager); - - void drawImplementation(osg::RenderInfo& renderInfo) const override; - - void dirty(size_t frameId) { mBufferData[frameId].dirty = true; } - - const fx::DispatchArray& getCurrentFrameData(size_t frame) { return mBufferData[frame % 2].data; } - - // Sets current frame pass data and stores copy of dispatch array to apply to next frame data - void setCurrentFrameData(size_t frameId, fx::DispatchArray&& data); - - void setMask(size_t frameId, bool underwater, bool exterior); - - void setSceneTexture(size_t frameId, osg::ref_ptr tex) { mBufferData[frameId].sceneTex = tex; } - - void setLDRSceneTexture(size_t frameId, osg::ref_ptr tex) - { - mBufferData[frameId].sceneTexLDR = tex; - } - - void setDepthTexture(size_t frameId, osg::ref_ptr tex) { mBufferData[frameId].depthTex = tex; } - - void setNormalsTexture(size_t frameId, osg::ref_ptr tex) - { - mBufferData[frameId].normalsTex = tex; - } - - void setHDR(size_t frameId, bool hdr) { mBufferData[frameId].hdr = hdr; } - - void setPostProcessing(size_t frameId, bool postprocessing) - { - mBufferData[frameId].postprocessing = postprocessing; - } - - const osg::ref_ptr& getSceneTexture(size_t frameId) const - { - return mBufferData[frameId].sceneTex; - } + PingPongCanvas( + Shader::ShaderManager& shaderManager, const std::shared_ptr& luminanceCalculator); void drawGeometry(osg::RenderInfo& renderInfo) const; - private: - void copyNewFrameData(size_t frameId) const; + void drawImplementation(osg::RenderInfo& renderInfo) const override; - mutable LuminanceCalculator mLuminanceCalculator; + void dirty() { mDirty = true; } + + void setDirtyAttachments(const std::vector& attachments) + { + mDirtyAttachments = attachments; + } + + const fx::DispatchArray& getPasses() { return mPasses; } + + void setPasses(fx::DispatchArray&& passes); + + void setMask(bool underwater, bool exterior); + + void setTextureScene(osg::ref_ptr tex) { mTextureScene = tex; } + + void setTextureDepth(osg::ref_ptr tex) { mTextureDepth = tex; } + + void setTextureNormals(osg::ref_ptr tex) { mTextureNormals = tex; } + + void setTextureDistortion(osg::ref_ptr tex) { mTextureDistortion = tex; } + + void setCalculateAvgLum(bool enabled) { mAvgLum = enabled; } + + void setPostProcessing(bool enabled) { mPostprocessing = enabled; } + + const osg::ref_ptr& getSceneTexture(size_t frameId) const { return mTextureScene; } + + private: + bool mAvgLum = false; + bool mPostprocessing = false; + + fx::DispatchArray mPasses; + fx::FlagsType mMask = 0; osg::ref_ptr mFallbackProgram; osg::ref_ptr mMultiviewResolveProgram; osg::ref_ptr mFallbackStateSet; osg::ref_ptr mMultiviewResolveStateSet; - mutable osg::ref_ptr mMultiviewResolveFramebuffer; - struct BufferData - { - bool dirty = false; - bool hdr = false; - bool postprocessing = true; + osg::ref_ptr mTextureScene; + osg::ref_ptr mTextureDepth; + osg::ref_ptr mTextureNormals; + osg::ref_ptr mTextureDistortion; - fx::DispatchArray data; - fx::FlagsType mask; - - osg::ref_ptr destination; - - osg::ref_ptr sceneTex; - osg::ref_ptr depthTex; - osg::ref_ptr sceneTexLDR; - osg::ref_ptr normalsTex; - }; - - mutable std::array mBufferData; - mutable std::array, 3> mFbos; + mutable bool mDirty = false; + mutable std::vector mDirtyAttachments; mutable osg::ref_ptr mRenderViewport; + mutable osg::ref_ptr mMultiviewResolveFramebuffer; + mutable osg::ref_ptr mDestinationFBO; + mutable std::array, 3> mFbos; + mutable std::shared_ptr mLuminanceCalculator; }; } diff --git a/apps/openmw/mwrender/pingpongcull.cpp b/apps/openmw/mwrender/pingpongcull.cpp index 8dfff5a60c..497c6c734a 100644 --- a/apps/openmw/mwrender/pingpongcull.cpp +++ b/apps/openmw/mwrender/pingpongcull.cpp @@ -21,7 +21,7 @@ namespace MWRender if (Stereo::getStereo()) { mViewportStateset = new osg::StateSet(); - mViewport = new osg::Viewport(0, 0, pp->renderWidth(), pp->renderHeight()); + mViewport = new osg::Viewport; mViewportStateset->setAttribute(mViewport); } } @@ -37,41 +37,31 @@ namespace MWRender size_t frame = cv->getTraversalNumber(); size_t frameId = frame % 2; - MWRender::PostProcessor* postProcessor - = dynamic_cast(cv->getCurrentCamera()->getUserData()); - if (!postProcessor) - throw std::runtime_error("PingPongCull: failed to get a PostProcessor!"); - if (Stereo::getStereo()) { auto& sm = Stereo::Manager::instance(); auto view = sm.getEye(cv); int index = view == Stereo::Eye::Right ? 1 : 0; auto projectionMatrix = sm.computeEyeProjection(index, true); - postProcessor->getStateUpdater()->setProjectionMatrix(projectionMatrix); + mPostProcessor->getStateUpdater()->setProjectionMatrix(projectionMatrix); } - postProcessor->getStateUpdater()->setViewMatrix(cv->getCurrentCamera()->getViewMatrix()); - postProcessor->getStateUpdater()->setPrevViewMatrix(mLastViewMatrix[0]); + mPostProcessor->getStateUpdater()->setViewMatrix(cv->getCurrentCamera()->getViewMatrix()); + mPostProcessor->getStateUpdater()->setPrevViewMatrix(mLastViewMatrix[0]); mLastViewMatrix[0] = cv->getCurrentCamera()->getViewMatrix(); - postProcessor->getStateUpdater()->setEyePos(cv->getEyePoint()); - postProcessor->getStateUpdater()->setEyeVec(cv->getLookVectorLocal()); + mPostProcessor->getStateUpdater()->setEyePos(cv->getEyePoint()); + mPostProcessor->getStateUpdater()->setEyeVec(cv->getLookVectorLocal()); - if (!postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)) + if (!mPostProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)) { - renderStage->setMultisampleResolveFramebufferObject(nullptr); - renderStage->setFrameBufferObject(nullptr); - } - else if (!postProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)) - { - renderStage->setFrameBufferObject(postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + renderStage->setFrameBufferObject(mPostProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); } else { renderStage->setMultisampleResolveFramebufferObject( - postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); - renderStage->setFrameBufferObject(postProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)); + mPostProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + renderStage->setFrameBufferObject(mPostProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)); // The MultiView patch has a bug where it does not update resolve layers if the resolve framebuffer is // changed. So we do blit manually in this case diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index d64e9651bc..8fdc0a45ca 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -18,18 +18,21 @@ #include #include #include -#include +#include #include #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwgui/postprocessorhud.hpp" +#include "distortion.hpp" #include "pingpongcull.hpp" +#include "renderbin.hpp" #include "renderingmanager.hpp" #include "sky.hpp" #include "transparentpass.hpp" @@ -103,6 +106,8 @@ namespace return Stereo::createMultiviewCompatibleAttachment(texture); } + + constexpr float DistortionRatio = 0.25; } namespace MWRender @@ -110,33 +115,76 @@ namespace MWRender PostProcessor::PostProcessor( RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs) : osg::Group() - , mEnableLiveReload(false) , mRootNode(rootNode) - , mSamples(Settings::Manager::getInt("antialiasing", "Video")) - , mDirty(false) - , mDirtyFrameId(0) + , mHUDCamera(new osg::Camera) , mRendering(rendering) , mViewer(viewer) , mVFS(vfs) - , mTriggerShaderReload(false) - , mReload(false) - , mEnabled(false) - , mUsePostProcessing(false) - , mSoftParticles(false) - , mDisableDepthPasses(false) - , mLastFrameNumber(0) - , mLastSimulationTime(0.f) - , mExteriorFlag(false) - , mUnderwater(false) - , mHDR(false) - , mNormals(false) - , mPrevNormals(false) - , mNormalsSupported(false) - , mPassLights(false) - , mPrevPassLights(false) + , mUsePostProcessing(Settings::postProcessing().mEnabled) + , mSamples(Settings::video().mAntialiasing) + , mPingPongCull(new PingPongCull(this)) + , mDistortionCallback(new DistortionCallback) { - mSoftParticles = Settings::Manager::getBool("soft particles", "Shaders"); - mUsePostProcessing = Settings::Manager::getBool("enabled", "Post Processing"); + auto& shaderManager = mRendering.getResourceSystem()->getSceneManager()->getShaderManager(); + + std::shared_ptr luminanceCalculator = std::make_shared(shaderManager); + + for (auto& canvas : mCanvases) + canvas = new PingPongCanvas(shaderManager, luminanceCalculator); + + mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER); + mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0)); + mHUDCamera->setClearMask(0); + mHUDCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); + mHUDCamera->setAllowEventFocus(false); + mHUDCamera->setViewport(0, 0, mWidth, mHeight); + mHUDCamera->setNodeMask(Mask_RenderToTexture); + mHUDCamera->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + mHUDCamera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mHUDCamera->addChild(mCanvases[0]); + mHUDCamera->addChild(mCanvases[1]); + mHUDCamera->setCullCallback(new HUDCullCallback); + mViewer->getCamera()->addCullCallback(mPingPongCull); + + // resolves the multisampled depth buffer and optionally draws an additional depth postpass + mTransparentDepthPostPass + = new TransparentDepthBinCallback(mRendering.getResourceSystem()->getSceneManager()->getShaderManager(), + Settings::postProcessing().mTransparentPostpass); + osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass); + + osg::ref_ptr distortionRenderBin + = new osgUtil::RenderBin(osgUtil::RenderBin::SORT_BACK_TO_FRONT); + // This is silly to have to do, but if nothing is drawn then the drawcallback is never called and the distortion + // texture will never be cleared + osg::ref_ptr dummyNodeToClear = new osg::Node; + dummyNodeToClear->setCullingActive(false); + dummyNodeToClear->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Distortion, "Distortion"); + rootNode->addChild(dummyNodeToClear); + distortionRenderBin->setDrawCallback(mDistortionCallback); + distortionRenderBin->getStateSet()->setDefine("DISTORTION", "1", osg::StateAttribute::ON); + + // Give the renderbin access to the opaque depth sampler so it can write its occlusion + // Distorted geometry is drawn with ALWAYS depth function and depths writes disbled. + const int unitSoftEffect + = shaderManager.reserveGlobalTextureUnits(Shader::ShaderManager::Slot::OpaqueDepthTexture); + distortionRenderBin->getStateSet()->addUniform(new osg::Uniform("opaqueDepthTex", unitSoftEffect)); + + osgUtil::RenderBin::addRenderBinPrototype("Distortion", distortionRenderBin); + + auto defines = shaderManager.getGlobalDefines(); + defines["distorionRTRatio"] = std::to_string(DistortionRatio); + shaderManager.setGlobalDefines(defines); + + createObjectsForFrame(0); + createObjectsForFrame(1); + + populateTechniqueFiles(); + + auto distortion = loadTechnique("internal_distortion"); + distortion->setInternal(true); + distortion->setLocked(true); + mInternalTechniques.push_back(distortion); osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext(); osg::GLExtensions* ext = gc->getState()->get(); @@ -156,27 +204,22 @@ namespace MWRender else Log(Debug::Error) << "'glDisablei' unsupported, pass normals will not be available to shaders."; - if (mSoftParticles) - { - for (int i = 0; i < 2; ++i) - { - if (Stereo::getMultiview()) - mTextures[i][Tex_OpaqueDepth] = new osg::Texture2DArray; - else - mTextures[i][Tex_OpaqueDepth] = new osg::Texture2D; - mTextures[i][Tex_OpaqueDepth]->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mTextures[i][Tex_OpaqueDepth]->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - } - } - mGLSLVersion = ext->glslLanguageVersion * 100; mUBO = ext->isUniformBufferObjectSupported && mGLSLVersion >= 330; mStateUpdater = new fx::StateUpdater(mUBO); - if (!Stereo::getStereo() && !SceneUtil::AutoDepth::isReversed() && !mSoftParticles && !mUsePostProcessing) - return; + addChild(mHUDCamera); + addChild(mRootNode); - enable(mUsePostProcessing); + mViewer->setSceneData(this); + mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + mViewer->getCamera()->getGraphicsContext()->setResizedCallback(new ResizedCallback(this)); + mViewer->getCamera()->setUserData(this); + + setCullCallback(mStateUpdater); + + if (mUsePostProcessing) + enable(); } PostProcessor::~PostProcessor() @@ -192,28 +235,14 @@ namespace MWRender if (Stereo::getStereo()) Stereo::Manager::instance().screenResolutionChanged(); - auto width = renderWidth(); - auto height = renderHeight(); - for (auto& technique : mTechniques) - { - for (auto& [name, rt] : technique->getRenderTargetsMap()) - { - const auto [w, h] = rt.mSize.get(width, height); - rt.mTarget->setTextureSize(w, h); - } - } - size_t frameId = frame() % 2; - createTexturesAndCamera(frameId); createObjectsForFrame(frameId); mRendering.updateProjectionMatrix(); - mRendering.setScreenRes(width, height); + mRendering.setScreenRes(renderWidth(), renderHeight()); - dirtyTechniques(); - - mPingPongCanvas->dirty(frameId); + dirtyTechniques(true); mDirty = true; mDirtyFrameId = !frameId; @@ -233,77 +262,20 @@ namespace MWRender } } - void PostProcessor::enable(bool usePostProcessing) + void PostProcessor::enable() { mReload = true; - mEnabled = true; - bool postPass = Settings::Manager::getBool("transparent postpass", "Post Processing"); - mUsePostProcessing = usePostProcessing; - - mDisableDepthPasses = !mSoftParticles && !postPass; - -#ifdef ANDROID - mDisableDepthPasses = true; -#endif - - if (!mDisableDepthPasses) - { - mTransparentDepthPostPass = new TransparentDepthBinCallback( - mRendering.getResourceSystem()->getSceneManager()->getShaderManager(), postPass); - osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass); - } - - if (mUsePostProcessing && mTechniqueFileMap.empty()) - { - populateTechniqueFiles(); - } - - createTexturesAndCamera(frame() % 2); - - removeChild(mHUDCamera); - removeChild(mRootNode); - - addChild(mHUDCamera); - addChild(mRootNode); - - mViewer->setSceneData(this); - mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - mViewer->getCamera()->getGraphicsContext()->setResizedCallback(new ResizedCallback(this)); - mViewer->getCamera()->setUserData(this); - - setCullCallback(mStateUpdater); - mHUDCamera->setCullCallback(new HUDCullCallback); + mUsePostProcessing = true; } void PostProcessor::disable() { - if (!mSoftParticles) - osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(nullptr); - - if (!SceneUtil::AutoDepth::isReversed() && !mSoftParticles) - { - removeChild(mHUDCamera); - setCullCallback(nullptr); - - mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER); - mViewer->getCamera()->getGraphicsContext()->setResizedCallback(nullptr); - mViewer->getCamera()->setUserData(nullptr); - - mEnabled = false; - } - mUsePostProcessing = false; mRendering.getSkyManager()->setSunglare(true); } void PostProcessor::traverse(osg::NodeVisitor& nv) { - if (!mEnabled) - { - osg::Group::traverse(nv); - return; - } - size_t frameId = nv.getTraversalNumber() % 2; if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR) @@ -316,33 +288,28 @@ namespace MWRender void PostProcessor::cull(size_t frameId, osgUtil::CullVisitor* cv) { - const auto& fbo = getFbo(FBO_Intercept, frameId); - if (fbo) + if (const auto& fbo = getFbo(FBO_Intercept, frameId)) { osgUtil::RenderStage* rs = cv->getRenderStage(); if (rs && rs->getMultisampleResolveFramebufferObject()) rs->setMultisampleResolveFramebufferObject(fbo); } - mPingPongCanvas->setPostProcessing(frameId, mUsePostProcessing); - mPingPongCanvas->setNormalsTexture(frameId, mNormals ? getTexture(Tex_Normal, frameId) : nullptr); - mPingPongCanvas->setMask(frameId, mUnderwater, mExteriorFlag); - mPingPongCanvas->setHDR(frameId, getHDR()); + mCanvases[frameId]->setPostProcessing(mUsePostProcessing); + mCanvases[frameId]->setTextureNormals(mNormals ? getTexture(Tex_Normal, frameId) : nullptr); + mCanvases[frameId]->setMask(mUnderwater, mExteriorFlag); + mCanvases[frameId]->setCalculateAvgLum(mHDR); - mPingPongCanvas->setSceneTexture(frameId, getTexture(Tex_Scene, frameId)); - if (mDisableDepthPasses) - mPingPongCanvas->setDepthTexture(frameId, getTexture(Tex_Depth, frameId)); - else - mPingPongCanvas->setDepthTexture(frameId, getTexture(Tex_OpaqueDepth, frameId)); + mCanvases[frameId]->setTextureScene(getTexture(Tex_Scene, frameId)); + mCanvases[frameId]->setTextureDepth(getTexture(Tex_OpaqueDepth, frameId)); + mCanvases[frameId]->setTextureDistortion(getTexture(Tex_Distortion, frameId)); - mPingPongCanvas->setLDRSceneTexture(frameId, getTexture(Tex_Scene_LDR, frameId)); + mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; + mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; + mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; - if (mTransparentDepthPostPass) - { - mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; - mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; - mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; - } + mDistortionCallback->setFBO(mFbos[frameId][FBO_Distortion], frameId); + mDistortionCallback->setOriginalFBO(mFbos[frameId][FBO_Primary], frameId); size_t frame = cv->getTraversalNumber(); @@ -356,9 +323,11 @@ namespace MWRender mStateUpdater->setSimulationTime(static_cast(stamp->getSimulationTime())); mStateUpdater->setDeltaSimulationTime(static_cast(stamp->getSimulationTime() - mLastSimulationTime)); + // Use a signed int because 'uint' type is not supported in GLSL 120 without extensions + mStateUpdater->setFrameNumber(static_cast(stamp->getFrameNumber())); mLastSimulationTime = stamp->getSimulationTime(); - for (const auto& dispatchNode : mPingPongCanvas->getCurrentFrameData(frame)) + for (const auto& dispatchNode : mCanvases[frameId]->getPasses()) { for (auto& uniform : dispatchNode.mHandle->getUniformMap()) { @@ -379,7 +348,7 @@ namespace MWRender for (auto& technique : mTechniques) { - if (technique->getStatus() == fx::Technique::Status::File_Not_exists) + if (!technique || technique->getStatus() == fx::Technique::Status::File_Not_exists) continue; const auto lastWriteTime = std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()]); @@ -424,13 +393,15 @@ namespace MWRender reloadIfRequired(); + mCanvases[frameId]->setNodeMask(~0u); + mCanvases[!frameId]->setNodeMask(0); + if (mDirty && mDirtyFrameId == frameId) { - createTexturesAndCamera(frameId); createObjectsForFrame(frameId); - mDirty = false; - mPingPongCanvas->setCurrentFrameData(frameId, fx::DispatchArray(mTemplateData)); + mDirty = false; + mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); } if ((mNormalsSupported && mNormals != mPrevNormals) || (mPassLights != mPrevPassLights)) @@ -440,10 +411,14 @@ namespace MWRender mViewer->stopThreading(); - auto& shaderManager = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager(); - auto defines = shaderManager.getGlobalDefines(); - defines["disableNormals"] = mNormals ? "0" : "1"; - shaderManager.setGlobalDefines(defines); + if (mNormalsSupported) + { + auto& shaderManager + = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager(); + auto defines = shaderManager.getGlobalDefines(); + defines["disableNormals"] = mNormals ? "0" : "1"; + shaderManager.setGlobalDefines(defines); + } mRendering.getLightRoot()->setCollectPPLights(mPassLights); mStateUpdater->bindPointLights(mPassLights ? mRendering.getLightRoot()->getPPLightsBuffer() : nullptr); @@ -451,7 +426,6 @@ namespace MWRender mViewer->startThreading(); - createTexturesAndCamera(frameId); createObjectsForFrame(frameId); mDirty = true; @@ -461,20 +435,55 @@ namespace MWRender void PostProcessor::createObjectsForFrame(size_t frameId) { - auto& fbos = mFbos[frameId]; auto& textures = mTextures[frameId]; - auto width = renderWidth(); - auto height = renderHeight(); - for (auto& tex : textures) + int width = renderWidth(); + int height = renderHeight(); + + for (osg::ref_ptr& texture : textures) { - if (!tex) - continue; - - Stereo::setMultiviewCompatibleTextureSize(tex, width, height); - tex->dirtyTextureObject(); + if (!texture) + { + if (Stereo::getMultiview()) + texture = new osg::Texture2DArray; + else + texture = new osg::Texture2D; + } + Stereo::setMultiviewCompatibleTextureSize(texture, width, height); + texture->setSourceFormat(GL_RGBA); + texture->setSourceType(GL_UNSIGNED_BYTE); + texture->setInternalFormat(GL_RGBA); + texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture::LINEAR); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + texture->setResizeNonPowerOfTwoHint(false); + Stereo::setMultiviewCompatibleTextureSize(texture, width, height); + texture->dirtyTextureObject(); } + textures[Tex_Normal]->setSourceFormat(GL_RGB); + textures[Tex_Normal]->setInternalFormat(GL_RGB); + + textures[Tex_Distortion]->setSourceFormat(GL_RGB); + textures[Tex_Distortion]->setInternalFormat(GL_RGB); + + Stereo::setMultiviewCompatibleTextureSize( + textures[Tex_Distortion], width * DistortionRatio, height * DistortionRatio); + textures[Tex_Distortion]->dirtyTextureObject(); + + auto setupDepth = [](osg::Texture* tex) { + tex->setSourceFormat(GL_DEPTH_STENCIL_EXT); + tex->setSourceType(SceneUtil::AutoDepth::depthSourceType()); + tex->setInternalFormat(SceneUtil::AutoDepth::depthInternalFormat()); + }; + + setupDepth(textures[Tex_Depth]); + setupDepth(textures[Tex_OpaqueDepth]); + textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); + + auto& fbos = mFbos[frameId]; + fbos[FBO_Primary] = new osg::FrameBufferObject; fbos[FBO_Primary]->setAttachment( osg::Camera::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); @@ -501,6 +510,7 @@ namespace MWRender auto normalRB = createFrameBufferAttachmentFromTemplate( Usage::RENDER_BUFFER, width, height, textures[Tex_Normal], mSamples); fbos[FBO_Multisample]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, normalRB); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, normalRB); } auto depthRB = createFrameBufferAttachmentFromTemplate( Usage::RENDER_BUFFER, width, height, textures[Tex_Depth], mSamples); @@ -524,12 +534,13 @@ namespace MWRender Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); } - if (textures[Tex_OpaqueDepth]) - { - fbos[FBO_OpaqueDepth] = new osg::FrameBufferObject; - fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, - Stereo::createMultiviewCompatibleAttachment(textures[Tex_OpaqueDepth])); - } + fbos[FBO_OpaqueDepth] = new osg::FrameBufferObject; + fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_OpaqueDepth])); + + fbos[FBO_Distortion] = new osg::FrameBufferObject; + fbos[FBO_Distortion]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Distortion])); #ifdef __APPLE__ if (textures[Tex_OpaqueDepth]) @@ -537,13 +548,12 @@ namespace MWRender osg::FrameBufferAttachment(new osg::RenderBuffer(textures[Tex_OpaqueDepth]->getTextureWidth(), textures[Tex_OpaqueDepth]->getTextureHeight(), textures[Tex_Scene]->getInternalFormat()))); #endif + + mCanvases[frameId]->dirty(); } - void PostProcessor::dirtyTechniques() + void PostProcessor::dirtyTechniques(bool dirtyAttachments) { - if (!isEnabled()) - return; - size_t frameId = frame() % 2; mDirty = true; @@ -556,9 +566,11 @@ namespace MWRender mNormals = false; mPassLights = false; + std::vector attachmentsToDirty; + for (const auto& technique : mTechniques) { - if (!technique->isValid()) + if (!technique || !technique->isValid()) continue; if (technique->getGLSLVersion() > mGLSLVersion) @@ -588,6 +600,7 @@ namespace MWRender node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerLastShader", Unit_LastShader)); node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerLastPass", Unit_LastPass)); node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDepth", Unit_Depth)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDistortion", Unit_Distortion)); if (mNormals) node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerNormals", Unit_Normals)); @@ -595,6 +608,8 @@ namespace MWRender if (technique->getHDR()) node.mRootStateSet->addUniform(new osg::Uniform("omw_EyeAdaptation", Unit_EyeAdaptation)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDistortion", Unit_Distortion)); + int texUnit = Unit_NextFree; // user-defined samplers @@ -621,8 +636,6 @@ namespace MWRender uniform->mName.c_str(), *type, uniform->getNumElements())); } - std::unordered_map renderTargetCache; - for (const auto& pass : technique->getPasses()) { int subTexUnit = texUnit; @@ -634,32 +647,47 @@ namespace MWRender if (!pass->getTarget().empty()) { - const auto& rt = technique->getRenderTargetsMap()[pass->getTarget()]; + auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()]; + subPass.mSize = renderTarget.mSize; + subPass.mRenderTexture = renderTarget.mTarget; + subPass.mMipMap = renderTarget.mMipMap; - const auto [w, h] = rt.mSize.get(renderWidth(), renderHeight()); + const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight()); + subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); - subPass.mRenderTexture = new osg::Texture2D(*rt.mTarget); - renderTargetCache[rt.mTarget] = subPass.mRenderTexture; subPass.mRenderTexture->setTextureSize(w, h); - subPass.mRenderTexture->setName(std::string(pass->getTarget())); - - if (rt.mMipMap) - subPass.mRenderTexture->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + subPass.mRenderTexture->dirtyTextureObject(); subPass.mRenderTarget = new osg::FrameBufferObject; subPass.mRenderTarget->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(subPass.mRenderTexture)); - subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } } - for (const auto& whitelist : pass->getRenderTargets()) + for (const auto& name : pass->getRenderTargets()) { - auto it = technique->getRenderTargetsMap().find(whitelist); - if (it != technique->getRenderTargetsMap().end() && renderTargetCache[it->second.mTarget]) + if (name.empty()) { - subPass.mStateSet->setTextureAttribute(subTexUnit, renderTargetCache[it->second.mTarget]); - subPass.mStateSet->addUniform(new osg::Uniform(std::string(it->first).c_str(), subTexUnit++)); + continue; } + + auto& renderTarget = technique->getRenderTargetsMap()[name]; + subPass.mStateSet->setTextureAttribute(subTexUnit, renderTarget.mTarget); + subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } + subTexUnit++; } node.mPasses.emplace_back(std::move(subPass)); @@ -670,39 +698,36 @@ namespace MWRender mTemplateData.emplace_back(std::move(node)); } - mPingPongCanvas->setCurrentFrameData(frameId, fx::DispatchArray(mTemplateData)); + mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); if (auto hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) hud->updateTechniques(); mRendering.getSkyManager()->setSunglare(sunglare); + + if (dirtyAttachments) + mCanvases[frameId]->setDirtyAttachments(attachmentsToDirty); } PostProcessor::Status PostProcessor::enableTechnique( std::shared_ptr technique, std::optional location) { - if (!isEnabled()) - { - Log(Debug::Warning) << "PostProcessing disabled, cannot load technique '" << technique->getName() << "'"; - return Status_Error; - } - if (!technique || technique->getLocked() || (location.has_value() && location.value() < 0)) return Status_Error; disableTechnique(technique, false); - int pos = std::min(location.value_or(mTechniques.size()), mTechniques.size()); + int pos = std::min(location.value_or(mTechniques.size()) + mInternalTechniques.size(), mTechniques.size()); mTechniques.insert(mTechniques.begin() + pos, technique); - dirtyTechniques(); + dirtyTechniques(Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug); return Status_Toggled; } PostProcessor::Status PostProcessor::disableTechnique(std::shared_ptr technique, bool dirty) { - if (technique->getLocked()) + if (!technique || technique->getLocked()) return Status_Error; auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); @@ -718,92 +743,17 @@ namespace MWRender bool PostProcessor::isTechniqueEnabled(const std::shared_ptr& technique) const { + if (!technique) + return false; + if (auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); it == mTechniques.end()) return false; return technique->isValid(); } - void PostProcessor::createTexturesAndCamera(size_t frameId) - { - auto& textures = mTextures[frameId]; - - auto width = renderWidth(); - auto height = renderHeight(); - - for (auto& texture : textures) - { - if (!texture) - { - if (Stereo::getMultiview()) - texture = new osg::Texture2DArray; - else - texture = new osg::Texture2D; - } - Stereo::setMultiviewCompatibleTextureSize(texture, width, height); - texture->setSourceFormat(GL_RGBA); - texture->setSourceType(GL_UNSIGNED_BYTE); - texture->setInternalFormat(GL_RGBA); - texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); - texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture::LINEAR); - texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - texture->setResizeNonPowerOfTwoHint(false); - } - - textures[Tex_Normal]->setSourceFormat(GL_RGB); - textures[Tex_Normal]->setInternalFormat(GL_RGB); - - auto setupDepth = [](osg::Texture* tex) { - tex->setSourceFormat(GL_DEPTH_STENCIL_EXT); - tex->setSourceType(SceneUtil::AutoDepth::depthSourceType()); - tex->setInternalFormat(SceneUtil::AutoDepth::depthInternalFormat()); - }; - - setupDepth(textures[Tex_Depth]); - - if (mDisableDepthPasses) - { - textures[Tex_OpaqueDepth] = nullptr; - } - else - { - setupDepth(textures[Tex_OpaqueDepth]); - textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); - } - - if (mHUDCamera) - return; - - mHUDCamera = new osg::Camera; - mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); - mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER); - mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0)); - mHUDCamera->setClearMask(0); - mHUDCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); - mHUDCamera->setAllowEventFocus(false); - mHUDCamera->setViewport(0, 0, mWidth, mHeight); - - mViewer->getCamera()->removeCullCallback(mPingPongCull); - mPingPongCull = new PingPongCull(this); - mViewer->getCamera()->addCullCallback(mPingPongCull); - - mPingPongCanvas = new PingPongCanvas(mRendering.getResourceSystem()->getSceneManager()->getShaderManager()); - mHUDCamera->addChild(mPingPongCanvas); - mHUDCamera->setNodeMask(Mask_RenderToTexture); - - mHUDCamera->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - mHUDCamera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - } - std::shared_ptr PostProcessor::loadTechnique(const std::string& name, bool loadNextFrame) { - if (!isEnabled()) - { - Log(Debug::Warning) << "PostProcessing disabled, cannot load technique '" << name << "'"; - return nullptr; - } - for (const auto& technique : mTemplates) if (Misc::StringUtils::ciEqual(technique->getName(), name)) return technique; @@ -834,14 +784,14 @@ namespace MWRender void PostProcessor::loadChain() { - if (!isEnabled()) - return; - mTechniques.clear(); - std::vector techniqueStrings = Settings::Manager::getStringArray("chain", "Post Processing"); + for (const auto& technique : mInternalTechniques) + { + mTechniques.push_back(technique); + } - for (auto& techniqueName : techniqueStrings) + for (const std::string& techniqueName : Settings::postProcessing().mChain.get()) { if (techniqueName.empty()) continue; @@ -858,12 +808,12 @@ namespace MWRender for (const auto& technique : mTechniques) { - if (!technique || technique->getDynamic()) + if (!technique || technique->getDynamic() || technique->getInternal()) continue; chain.push_back(technique->getName()); } - Settings::Manager::setStringArray("chain", "Post Processing", chain); + Settings::postProcessing().mChain.set(chain); } void PostProcessor::toggleMode() @@ -871,13 +821,13 @@ namespace MWRender for (auto& technique : mTemplates) technique->compile(); - dirtyTechniques(); + dirtyTechniques(true); } void PostProcessor::disableDynamicShaders() { for (auto& technique : mTechniques) - if (technique->getDynamic()) + if (technique && technique->getDynamic()) disableTechnique(technique); } diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index fa230ec2e8..2630467f95 100644 --- a/apps/openmw/mwrender/postprocessor.hpp +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -50,12 +50,13 @@ namespace MWRender class PingPongCull; class PingPongCanvas; class TransparentDepthBinCallback; + class DistortionCallback; class PostProcessor : public osg::Group { public: - using FBOArray = std::array, 5>; - using TextureArray = std::array, 5>; + using FBOArray = std::array, 6>; + using TextureArray = std::array, 6>; using TechniqueList = std::vector>; enum TextureIndex @@ -64,7 +65,8 @@ namespace MWRender Tex_Scene_LDR, Tex_Depth, Tex_OpaqueDepth, - Tex_Normal + Tex_Normal, + Tex_Distortion, }; enum FBOIndex @@ -73,7 +75,8 @@ namespace MWRender FBO_Multisample, FBO_FirstPerson, FBO_OpaqueDepth, - FBO_Intercept + FBO_Intercept, + FBO_Distortion, }; enum TextureUnits @@ -83,6 +86,7 @@ namespace MWRender Unit_Depth, Unit_EyeAdaptation, Unit_Normals, + Unit_Distortion, Unit_NextFree }; @@ -115,14 +119,14 @@ namespace MWRender return mFbos[frameId][FBO_Multisample] ? mFbos[frameId][FBO_Multisample] : mFbos[frameId][FBO_Primary]; } + osg::ref_ptr getHUDCamera() { return mHUDCamera; } + osg::ref_ptr getStateUpdater() { return mStateUpdater; } const TechniqueList& getTechniques() { return mTechniques; } const TechniqueList& getTemplates() const { return mTemplates; } - osg::ref_ptr getCanvas() { return mPingPongCanvas; } - const auto& getTechniqueMap() const { return mTechniqueFileMap; } void resize(); @@ -173,15 +177,11 @@ namespace MWRender std::shared_ptr loadTechnique(const std::string& name, bool loadNextFrame = false); - bool isEnabled() const { return mUsePostProcessing && mEnabled; } - - bool softParticlesEnabled() const { return mSoftParticles; } - - bool getHDR() const { return mHDR; } + bool isEnabled() const { return mUsePostProcessing; } void disable(); - void enable(bool usePostProcessing = true); + void enable(); void setRenderTargetSize(int width, int height) { @@ -196,7 +196,7 @@ namespace MWRender void triggerShaderReload(); - bool mEnableLiveReload; + bool mEnableLiveReload = false; void loadChain(); void saveChain(); @@ -208,11 +208,7 @@ namespace MWRender void createObjectsForFrame(size_t frameId); - void createTexturesAndCamera(size_t frameId); - - void reloadMainPass(fx::Technique& technique); - - void dirtyTechniques(); + void dirtyTechniques(bool dirtyAttachments = false); void update(size_t frameId); @@ -231,46 +227,43 @@ namespace MWRender TechniqueList mTechniques; TechniqueList mTemplates; TechniqueList mQueuedTemplates; + TechniqueList mInternalTechniques; std::unordered_map mTechniqueFileMap; - int mSamples; - - bool mDirty; - size_t mDirtyFrameId; - RenderingManager& mRendering; osgViewer::Viewer* mViewer; const VFS::Manager* mVFS; - bool mTriggerShaderReload; - bool mReload; - bool mEnabled; - bool mUsePostProcessing; - bool mSoftParticles; - bool mDisableDepthPasses; + size_t mDirtyFrameId = 0; + size_t mLastFrameNumber = 0; + float mLastSimulationTime = 0.f; - size_t mLastFrameNumber; - float mLastSimulationTime; + bool mDirty = false; + bool mReload = true; + bool mTriggerShaderReload = false; + bool mUsePostProcessing = false; + + bool mUBO = false; + bool mHDR = false; + bool mNormals = false; + bool mUnderwater = false; + bool mPassLights = false; + bool mPrevNormals = false; + bool mExteriorFlag = false; + bool mNormalsSupported = false; + bool mPrevPassLights = false; - bool mExteriorFlag; - bool mUnderwater; - bool mHDR; - bool mNormals; - bool mPrevNormals; - bool mNormalsSupported; - bool mPassLights; - bool mPrevPassLights; - bool mUBO; int mGLSLVersion; + int mWidth; + int mHeight; + int mSamples; osg::ref_ptr mStateUpdater; osg::ref_ptr mPingPongCull; - osg::ref_ptr mPingPongCanvas; + std::array, 2> mCanvases; osg::ref_ptr mTransparentDepthPostPass; - - int mWidth; - int mHeight; + osg::ref_ptr mDistortionCallback; fx::DispatchArray mTemplateData; }; diff --git a/apps/openmw/mwrender/precipitationocclusion.cpp b/apps/openmw/mwrender/precipitationocclusion.cpp index e7c9245fef..31712410b8 100644 --- a/apps/openmw/mwrender/precipitationocclusion.cpp +++ b/apps/openmw/mwrender/precipitationocclusion.cpp @@ -1,5 +1,7 @@ #include "precipitationocclusion.hpp" +#include + #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -112,23 +115,26 @@ namespace MWRender mCamera->attach(osg::Camera::DEPTH_BUFFER, mDepthTexture); mCamera->addChild(mSceneNode); mCamera->setSmallFeatureCullingPixelSize( - Settings::Manager::getFloat("weather particle occlusion small feature culling pixel size", "Shaders")); + Settings::shaders().mWeatherParticleOcclusionSmallFeatureCullingPixelSize); SceneUtil::setCameraClearDepth(mCamera); } void PrecipitationOccluder::update() { + if (!mRange.has_value()) + return; + const osg::Vec3 pos = mSceneCamera->getInverseViewMatrix().getTrans(); - const float zmin = pos.z() - mRange.z() - Constants::CellSizeInUnits; - const float zmax = pos.z() + mRange.z() + Constants::CellSizeInUnits; + const float zmin = pos.z() - mRange->z() - Constants::CellSizeInUnits; + const float zmax = pos.z() + mRange->z() + Constants::CellSizeInUnits; const float near = 0; const float far = zmax - zmin; - const float left = -mRange.x() / 2; + const float left = -mRange->x() / 2; const float right = -left; - const float top = mRange.y() / 2; + const float top = mRange->y() / 2; const float bottom = -top; if (SceneUtil::AutoDepth::isReversed()) @@ -162,10 +168,14 @@ namespace MWRender mSkyCullCallback = nullptr; mRootNode->removeChild(mCamera); + mRange = std::nullopt; } void PrecipitationOccluder::updateRange(const osg::Vec3f range) { + assert(range.x() != 0); + assert(range.y() != 0); + assert(range.z() != 0); const osg::Vec3f margin = { -50, -50, 0 }; mRange = range - margin; } diff --git a/apps/openmw/mwrender/precipitationocclusion.hpp b/apps/openmw/mwrender/precipitationocclusion.hpp index 26114ed42f..9d2992637e 100644 --- a/apps/openmw/mwrender/precipitationocclusion.hpp +++ b/apps/openmw/mwrender/precipitationocclusion.hpp @@ -4,6 +4,8 @@ #include #include +#include + namespace MWRender { class PrecipitationOccluder @@ -27,7 +29,7 @@ namespace MWRender osg::ref_ptr mCamera; osg::ref_ptr mSceneCamera; osg::ref_ptr mDepthTexture; - osg::Vec3f mRange; + std::optional mRange; }; } diff --git a/apps/openmw/mwrender/recastmesh.cpp b/apps/openmw/mwrender/recastmesh.cpp index 8edbc8568d..30cfa82493 100644 --- a/apps/openmw/mwrender/recastmesh.cpp +++ b/apps/openmw/mwrender/recastmesh.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -16,9 +17,22 @@ namespace MWRender { + namespace + { + osg::ref_ptr makeDebugDrawStateSet() + { + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + + return stateSet; + } + } + RecastMesh::RecastMesh(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) + , mGroupStateSet(SceneUtil::makeDetourGroupStateSet()) + , mDebugDrawStateSet(makeDebugDrawStateSet()) { } @@ -55,11 +69,16 @@ namespace MWRender if (it->second.mVersion != tile->second->getVersion()) { - const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings.mRecast); - MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + const osg::ref_ptr group + = SceneUtil::createRecastMeshGroup(*tile->second, settings.mRecast, mDebugDrawStateSet); group->setNodeMask(Mask_Debug); + group->setStateSet(mGroupStateSet); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + mRootNode->removeChild(it->second.mValue); mRootNode->addChild(group); + it->second.mValue = group; it->second.mVersion = tile->second->getVersion(); } @@ -67,14 +86,26 @@ namespace MWRender ++it; } - for (const auto& tile : tiles) + for (const auto& [position, mesh] : tiles) { - if (mGroups.count(tile.first)) - continue; - const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings.mRecast); - MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + const auto it = mGroups.find(position); + + if (it != mGroups.end()) + { + if (it->second.mVersion == mesh->getVersion()) + continue; + + mRootNode->removeChild(it->second.mValue); + } + + const osg::ref_ptr group + = SceneUtil::createRecastMeshGroup(*mesh, settings.mRecast, mDebugDrawStateSet); group->setNodeMask(Mask_Debug); - mGroups.emplace(tile.first, Group{ tile.second->getVersion(), group }); + group->setStateSet(mGroupStateSet); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + + mGroups.insert_or_assign(it, position, Group{ mesh->getVersion(), group }); mRootNode->addChild(group); } } diff --git a/apps/openmw/mwrender/recastmesh.hpp b/apps/openmw/mwrender/recastmesh.hpp index 2a45d67c6f..5fadd0b76a 100644 --- a/apps/openmw/mwrender/recastmesh.hpp +++ b/apps/openmw/mwrender/recastmesh.hpp @@ -6,12 +6,11 @@ #include -#include - namespace osg { class Group; class Geometry; + class StateSet; } namespace DetourNavigator @@ -49,6 +48,8 @@ namespace MWRender osg::ref_ptr mRootNode; bool mEnabled; std::map mGroups; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; }; } diff --git a/apps/openmw/mwrender/renderbin.hpp b/apps/openmw/mwrender/renderbin.hpp index c14f611426..6f4ae0819b 100644 --- a/apps/openmw/mwrender/renderbin.hpp +++ b/apps/openmw/mwrender/renderbin.hpp @@ -13,7 +13,8 @@ namespace MWRender RenderBin_DepthSorted = 10, // osg::StateSet::TRANSPARENT_BIN RenderBin_OcclusionQuery = 11, RenderBin_FirstPerson = 12, - RenderBin_SunGlare = 13 + RenderBin_SunGlare = 13, + RenderBin_Distortion = 14, }; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index f9058fa18a..fcb2d18e4c 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -59,6 +59,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/groundcoverstore.hpp" +#include "../mwworld/scene.hpp" #include "../mwgui/postprocessorhud.hpp" @@ -82,6 +83,7 @@ #include "screenshotmanager.hpp" #include "sky.hpp" #include "terrainstorage.hpp" +#include "util.hpp" #include "vismask.hpp" #include "water.hpp" @@ -281,22 +283,22 @@ namespace MWRender { try { - for (std::vector::const_iterator it = mModels.begin(); it != mModels.end(); ++it) - mResourceSystem->getSceneManager()->getTemplate(*it); - for (std::vector::const_iterator it = mTextures.begin(); it != mTextures.end(); ++it) - mResourceSystem->getImageManager()->getImage(*it); - for (std::vector::const_iterator it = mKeyframes.begin(); it != mKeyframes.end(); ++it) - mResourceSystem->getKeyframeManager()->get(*it); + for (const VFS::Path::Normalized& v : mModels) + mResourceSystem->getSceneManager()->getTemplate(v); + for (const VFS::Path::Normalized& v : mTextures) + mResourceSystem->getImageManager()->getImage(v); + for (const VFS::Path::Normalized& v : mKeyframes) + mResourceSystem->getKeyframeManager()->get(v); } - catch (std::exception&) + catch (const std::exception& e) { - // ignore error (will be shown when these are needed proper) + Log(Debug::Warning) << "Failed to preload common assets: " << e.what(); } } - std::vector mModels; - std::vector mTextures; - std::vector mKeyframes; + std::vector mModels; + std::vector mTextures; + std::vector mKeyframes; private: Resource::ResourceSystem* mResourceSystem; @@ -312,7 +314,6 @@ namespace MWRender , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) , mNavigator(navigator) - , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) // TODO: Near clip should not need to be bounded like this, but too small values break OSG shadow calculations // CPU-side. See issue: #6072 @@ -325,46 +326,83 @@ namespace MWRender , mGroundCoverStore(groundcoverStore) { bool reverseZ = SceneUtil::AutoDepth::isReversed(); - auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString( - Settings::Manager::getString("lighting method", "Shaders")); + const SceneUtil::LightingMethod lightingMethod = Settings::shaders().mLightingMethod; resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); - // Shadows and radial fog have problems with fixed-function mode. - bool forceShaders = Settings::fog().mRadialFog || Settings::fog().mExponentialFog - || Settings::Manager::getBool("soft particles", "Shaders") - || Settings::Manager::getBool("force shaders", "Shaders") - || Settings::Manager::getBool("enable shadows", "Shadows") - || lightingMethod != SceneUtil::LightingMethod::FFP || reverseZ || mSkyBlending || Stereo::getMultiview(); - resourceSystem->getSceneManager()->setForceShaders(forceShaders); + // Figure out which pipeline must be used by default and inform the user + bool forceShaders = Settings::shaders().mForceShaders; + { + std::vector requesters; + if (!forceShaders) + { + if (Settings::fog().mRadialFog) + requesters.push_back("radial fog"); + if (Settings::fog().mExponentialFog) + requesters.push_back("exponential fog"); + if (mSkyBlending) + requesters.push_back("sky blending"); + if (Settings::shaders().mSoftParticles) + requesters.push_back("soft particles"); + if (Settings::shadows().mEnableShadows) + requesters.push_back("shadows"); + if (lightingMethod != SceneUtil::LightingMethod::FFP) + requesters.push_back("lighting method"); + if (reverseZ) + requesters.push_back("reverse-Z depth buffer"); + if (Stereo::getMultiview()) + requesters.push_back("stereo multiview"); + + if (!requesters.empty()) + forceShaders = true; + } + + if (forceShaders) + { + std::string message = "Using rendering with shaders by default"; + if (requesters.empty()) + { + message += " (forced)"; + } + else + { + message += ", requested by:"; + for (size_t i = 0; i < requesters.size(); i++) + message += "\n - " + requesters[i]; + } + Log(Debug::Info) << message; + } + else + { + Log(Debug::Info) << "Using fixed-function rendering by default"; + } + } + + resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped - resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); - resourceSystem->getSceneManager()->setAutoUseNormalMaps( - Settings::Manager::getBool("auto use object normal maps", "Shaders")); - resourceSystem->getSceneManager()->setNormalMapPattern( - Settings::Manager::getString("normal map pattern", "Shaders")); - resourceSystem->getSceneManager()->setNormalHeightMapPattern( - Settings::Manager::getString("normal height map pattern", "Shaders")); - resourceSystem->getSceneManager()->setAutoUseSpecularMaps( - Settings::Manager::getBool("auto use object specular maps", "Shaders")); - resourceSystem->getSceneManager()->setSpecularMapPattern( - Settings::Manager::getString("specular map pattern", "Shaders")); + resourceSystem->getSceneManager()->setClampLighting(Settings::shaders().mClampLighting); + resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::shaders().mAutoUseObjectNormalMaps); + resourceSystem->getSceneManager()->setNormalMapPattern(Settings::shaders().mNormalMapPattern); + resourceSystem->getSceneManager()->setNormalHeightMapPattern(Settings::shaders().mNormalHeightMapPattern); + resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::shaders().mAutoUseObjectSpecularMaps); + resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::shaders().mSpecularMapPattern); resourceSystem->getSceneManager()->setApplyLightingToEnvMaps( - Settings::Manager::getBool("apply lighting to environment maps", "Shaders")); - resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage( - Settings::Manager::getBool("antialias alpha test", "Shaders") - && Settings::Manager::getInt("antialiasing", "Video") > 1); + Settings::shaders().mApplyLightingToEnvironmentMaps); + resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(shouldAddMSAAIntermediateTarget()); resourceSystem->getSceneManager()->setAdjustCoverageForAlphaTest( - Settings::Manager::getBool("adjust coverage for alpha test", "Shaders")); + Settings::shaders().mAdjustCoverageForAlphaTest); // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this // depends on support for various OpenGL extensions. - osg::ref_ptr sceneRoot - = new SceneUtil::LightManager(lightingMethod == SceneUtil::LightingMethod::FFP); + osg::ref_ptr sceneRoot = new SceneUtil::LightManager(SceneUtil::LightSettings{ + .mLightingMethod = lightingMethod, + .mMaxLights = Settings::shaders().mMaxLights, + .mMaximumLightDistance = Settings::shaders().mMaximumLightDistance, + .mLightFadeStart = Settings::shaders().mLightFadeStart, + .mLightBoundsMultiplier = Settings::shaders().mLightBoundsMultiplier, + }); resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); - mMinimumAmbientLuminance - = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; @@ -373,22 +411,22 @@ namespace MWRender sceneRoot->setName("Scene Root"); int shadowCastingTraversalMask = Mask_Scene; - if (Settings::Manager::getBool("actor shadows", "Shadows")) + if (Settings::shadows().mActorShadows) shadowCastingTraversalMask |= Mask_Actor; - if (Settings::Manager::getBool("player shadows", "Shadows")) + if (Settings::shadows().mPlayerShadows) shadowCastingTraversalMask |= Mask_Player; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; - if (Settings::Manager::getBool("object shadows", "Shadows")) + if (Settings::shadows().mObjectShadows) shadowCastingTraversalMask |= (Mask_Object | Mask_Static); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) + if (Settings::shadows().mTerrainShadows) shadowCastingTraversalMask |= Mask_Terrain; mShadowManager = std::make_unique(sceneRoot, mRootNode, shadowCastingTraversalMask, - indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static, + indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static, Settings::shadows(), mResourceSystem->getSceneManager()->getShaderManager()); - Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); + Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(Settings::shadows()); Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); @@ -396,15 +434,15 @@ namespace MWRender for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++) globalDefines[itr->first] = itr->second; - globalDefines["forcePPL"] = Settings::Manager::getBool("force per pixel lighting", "Shaders") ? "1" : "0"; - globalDefines["clamp"] = Settings::Manager::getBool("clamp lighting", "Shaders") ? "1" : "0"; - globalDefines["preLightEnv"] - = Settings::Manager::getBool("apply lighting to environment maps", "Shaders") ? "1" : "0"; + globalDefines["forcePPL"] = Settings::shaders().mForcePerPixelLighting ? "1" : "0"; + globalDefines["clamp"] = Settings::shaders().mClampLighting ? "1" : "0"; + globalDefines["preLightEnv"] = Settings::shaders().mApplyLightingToEnvironmentMaps ? "1" : "0"; + globalDefines["classicFalloff"] = Settings::shaders().mClassicFalloff ? "1" : "0"; const bool exponentialFog = Settings::fog().mExponentialFog; globalDefines["radialFog"] = (exponentialFog || Settings::fog().mRadialFog) ? "1" : "0"; globalDefines["exponentialFog"] = exponentialFog ? "1" : "0"; globalDefines["skyBlending"] = mSkyBlending ? "1" : "0"; - globalDefines["refraction_enabled"] = "0"; + globalDefines["waterRefraction"] = "0"; globalDefines["useGPUShader4"] = "0"; globalDefines["useOVR_multiview"] = "0"; globalDefines["numViews"] = "1"; @@ -425,13 +463,10 @@ namespace MWRender // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); - mNavMesh = std::make_unique(mRootNode, mWorkQueue, - Settings::Manager::getBool("enable nav mesh render", "Navigator"), - parseNavMeshMode(Settings::Manager::getString("nav mesh render mode", "Navigator"))); - mActorsPaths = std::make_unique( - mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator")); - mRecastMesh = std::make_unique( - mRootNode, Settings::Manager::getBool("enable recast mesh render", "Navigator")); + mNavMesh = std::make_unique(mRootNode, mWorkQueue, Settings::navigator().mEnableNavMeshRender, + Settings::navigator().mNavMeshRenderMode); + mActorsPaths = std::make_unique(mRootNode, Settings::navigator().mEnableAgentsPathsRender); + mRecastMesh = std::make_unique(mRootNode, Settings::navigator().mEnableRecastMeshRender); mPathgrid = std::make_unique(mRootNode); mObjects = std::make_unique(mResourceSystem, sceneRoot, unrefQueue); @@ -442,17 +477,19 @@ namespace MWRender mViewer->getIncrementalCompileOperation()->setTargetFrameRate(Settings::cells().mTargetFramerate); } - mDebugDraw - = std::make_unique(mResourceSystem->getSceneManager()->getShaderManager(), mRootNode); + mDebugDraw = new Debug::DebugDrawer(mResourceSystem->getSceneManager()->getShaderManager()); + mDebugDraw->setNodeMask(Mask_Debug); + sceneRoot->addChild(mDebugDraw); + mResourceSystem->getSceneManager()->setIncrementalCompileOperation(mViewer->getIncrementalCompileOperation()); mEffectManager = std::make_unique(sceneRoot, mResourceSystem); - const std::string& normalMapPattern = Settings::Manager::getString("normal map pattern", "Shaders"); - const std::string& heightMapPattern = Settings::Manager::getString("normal height map pattern", "Shaders"); - const std::string& specularMapPattern = Settings::Manager::getString("terrain specular map pattern", "Shaders"); - const bool useTerrainNormalMaps = Settings::Manager::getBool("auto use terrain normal maps", "Shaders"); - const bool useTerrainSpecularMaps = Settings::Manager::getBool("auto use terrain specular maps", "Shaders"); + const std::string& normalMapPattern = Settings::shaders().mNormalMapPattern; + const std::string& heightMapPattern = Settings::shaders().mNormalHeightMapPattern; + const std::string& specularMapPattern = Settings::shaders().mTerrainSpecularMapPattern; + const bool useTerrainNormalMaps = Settings::shaders().mAutoUseTerrainNormalMaps; + const bool useTerrainSpecularMaps = Settings::shaders().mAutoUseTerrainSpecularMaps; mTerrainStorage = std::make_unique(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps); @@ -475,8 +512,9 @@ namespace MWRender resourceSystem->getSceneManager()->setOpaqueDepthTex( mPostProcessor->getTexture(PostProcessor::Tex_OpaqueDepth, 0), mPostProcessor->getTexture(PostProcessor::Tex_OpaqueDepth, 1)); - resourceSystem->getSceneManager()->setSoftParticles(mPostProcessor->softParticlesEnabled()); + resourceSystem->getSceneManager()->setSoftParticles(Settings::shaders().mSoftParticles); resourceSystem->getSceneManager()->setSupportsNormalsRT(mPostProcessor->getSupportsNormalsRT()); + resourceSystem->getSceneManager()->setWeatherParticleOcclusion(Settings::shaders().mWeatherParticleOcclusion); // water goes after terrain for correct waterculling order mWater = std::make_unique( @@ -484,8 +522,7 @@ namespace MWRender mCamera = std::make_unique(mViewer->getCamera()); - mScreenshotManager - = std::make_unique(viewer, mRootNode, sceneRoot, mResourceSystem, mWater.get()); + mScreenshotManager = std::make_unique(viewer); mViewer->setLightingMode(osgViewer::View::NO_LIGHT); @@ -511,6 +548,9 @@ namespace MWRender sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("distortionStrength", 0.f)); + + resourceSystem->getSceneManager()->setUpNormalsRTForStateSet(sceneRoot->getOrCreateStateSet(), true); mFog = std::make_unique(); @@ -543,8 +583,7 @@ namespace MWRender MWBase::Environment::get().getWindowManager()->setCullMask(mask); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); - Nif::Reader::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - Nif::Reader::setWriteNifDebugLog(Settings::Manager::getBool("write nif debug log", "Models")); + Nif::Reader::setLoadUnsupportedFiles(Settings::models().mLoadUnsupportedNifFiles); mStateUpdater->setFogEnd(mViewDistance); @@ -606,15 +645,15 @@ namespace MWRender mSky->listAssetsToPreload(workItem->mModels, workItem->mTextures); mWater->listAssetsToPreload(workItem->mTextures); - workItem->mModels.push_back(Settings::Manager::getString("xbaseanim", "Models")); - workItem->mModels.push_back(Settings::Manager::getString("xbaseanim1st", "Models")); - workItem->mModels.push_back(Settings::Manager::getString("xbaseanimfemale", "Models")); - workItem->mModels.push_back(Settings::Manager::getString("xargonianswimkna", "Models")); + workItem->mModels.push_back(Settings::models().mXbaseanim); + workItem->mModels.push_back(Settings::models().mXbaseanim1st); + workItem->mModels.push_back(Settings::models().mXbaseanimfemale); + workItem->mModels.push_back(Settings::models().mXargonianswimkna); - workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimkf", "Models")); - workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanim1stkf", "Models")); - workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimfemalekf", "Models")); - workItem->mKeyframes.push_back(Settings::Manager::getString("xargonianswimknakf", "Models")); + workItem->mKeyframes.push_back(Settings::models().mXbaseanimkf); + workItem->mKeyframes.push_back(Settings::models().mXbaseanim1stkf); + workItem->mKeyframes.push_back(Settings::models().mXbaseanimfemalekf); + workItem->mKeyframes.push_back(Settings::models().mXargonianswimknakf); workItem->mTextures.emplace_back("textures/_land_default.dds"); @@ -646,11 +685,6 @@ namespace MWRender updateAmbient(); } - void RenderingManager::skySetDate(int day, int month) - { - mSky->setDate(day, month); - } - int RenderingManager::skyGetMasserPhase() const { return mSky->getMasserPhase(); @@ -671,7 +705,7 @@ namespace MWRender bool isInterior = !cell.isExterior() && !cell.isQuasiExterior(); bool needsAdjusting = false; if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) - needsAdjusting = isInterior; + needsAdjusting = isInterior && !Settings::shaders().mClassicFalloff; osg::Vec4f ambient = SceneUtil::colourFromRGB(cell.getMood().mAmbiantColor); @@ -683,15 +717,16 @@ namespace MWRender // we already work in linear RGB so no conversions are needed for the luminosity function float relativeLuminance = pR * ambient.r() + pG * ambient.g() + pB * ambient.b(); - if (relativeLuminance < mMinimumAmbientLuminance) + const float minimumAmbientLuminance = Settings::shaders().mMinimumInteriorBrightness; + if (relativeLuminance < minimumAmbientLuminance) { // brighten ambient so it reaches the minimum threshold but no more, we want to mess with content data // as least we can if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) ambient = osg::Vec4( - mMinimumAmbientLuminance, mMinimumAmbientLuminance, mMinimumAmbientLuminance, ambient.a()); + minimumAmbientLuminance, minimumAmbientLuminance, minimumAmbientLuminance, ambient.a()); else - ambient *= mMinimumAmbientLuminance / relativeLuminance; + ambient *= minimumAmbientLuminance / relativeLuminance; } } @@ -700,8 +735,9 @@ namespace MWRender osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell.getMood().mDirectionalColor); setSunColour(diffuse, diffuse, 1.f); - - const osg::Vec4f interiorSunPos = osg::Vec4f(-0.15f, 0.15f, 1.f, 0.f); + // This is total nonsense but it's what Morrowind uses + static const osg::Vec4f interiorSunPos + = osg::Vec4f(-1.f, osg::DegreesToRadians(45.f), osg::DegreesToRadians(45.f), 0.f); mPostProcessor->getStateUpdater()->setSunPos(interiorSunPos, false); mSunLight->setPosition(interiorSunPos); } @@ -710,7 +746,7 @@ namespace MWRender { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(diffuse); - mSunLight->setSpecular(specular); + mSunLight->setSpecular(osg::Vec4f(specular.x(), specular.y(), specular.z(), specular.w() * sunVis)); mPostProcessor->getStateUpdater()->setSunColor(diffuse); mPostProcessor->getStateUpdater()->setSunVis(sunVis); @@ -722,9 +758,12 @@ namespace MWRender // need to wrap this in a StateUpdater? mSunLight->setPosition(osg::Vec4(position.x(), position.y(), position.z(), 0)); + // The sun is not synchronized with the sunlight because sunlight origin can't reach the horizon + // This is based on exterior sun orbit and won't make sense for interiors, see WeatherManager::update + position.z() = 400.f - std::abs(position.x()); mSky->setSunDirection(position); - mPostProcessor->getStateUpdater()->setSunPos(mSunLight->getPosition(), mNight); + mPostProcessor->getStateUpdater()->setSunPos(osg::Vec4f(position, 0.f), mNight); } void RenderingManager::addCell(const MWWorld::CellStore* store) @@ -778,7 +817,7 @@ namespace MWRender if (enabled) mShadowManager->enableOutdoorMode(); else - mShadowManager->enableIndoorMode(); + mShadowManager->enableIndoorMode(Settings::shadows()); mPostProcessor->getStateUpdater()->setIsInterior(!enabled); } @@ -855,6 +894,7 @@ namespace MWRender float rainIntensity = mSky->getPrecipitationAlpha(); mWater->setRainIntensity(rainIntensity); + mWater->setRainRipplesEnabled(mSky->getRainRipplesEnabled()); mWater->update(dt, paused); if (!paused) @@ -973,19 +1013,6 @@ namespace MWRender mScreenshotManager->screenshot(image, w, h); } - bool RenderingManager::screenshot360(osg::Image* image) - { - if (mCamera->isVanityOrPreviewModeEnabled()) - { - Log(Debug::Warning) << "Spherical screenshots are not allowed in preview mode."; - return false; - } - - mScreenshotManager->screenshot360(image); - - return true; - } - osg::Vec4f RenderingManager::getScreenBounds(const osg::BoundingBox& worldbb) { if (!worldbb.valid()) @@ -1016,34 +1043,44 @@ namespace MWRender return osg::Vec4f(min_x, min_y, max_x, max_y); } - RenderingManager::RayResult getIntersectionResult(osgUtil::LineSegmentIntersector* intersector) + RenderingManager::RayResult getIntersectionResult(osgUtil::LineSegmentIntersector* intersector, + const osg::ref_ptr& visitor, std::span ignoreList = {}) { + constexpr auto nonObjectWorldMask = Mask_Terrain | Mask_Water; RenderingManager::RayResult result; result.mHit = false; result.mRatio = 0; - if (intersector->containsIntersections()) - { - result.mHit = true; - osgUtil::LineSegmentIntersector::Intersection intersection = intersector->getFirstIntersection(); - result.mHitPointWorld = intersection.getWorldIntersectPoint(); - result.mHitNormalWorld = intersection.getWorldIntersectNormal(); - result.mRatio = intersection.ratio; + if (!intersector->containsIntersections()) + return result; + auto test = [&](const osgUtil::LineSegmentIntersector::Intersection& intersection) { PtrHolder* ptrHolder = nullptr; std::vector refnumMarkers; + bool hitNonObjectWorld = false; for (osg::NodePath::const_iterator it = intersection.nodePath.begin(); it != intersection.nodePath.end(); ++it) { + const auto& nodeMask = (*it)->getNodeMask(); + if (!hitNonObjectWorld) + hitNonObjectWorld = nodeMask & nonObjectWorldMask; + osg::UserDataContainer* userDataContainer = (*it)->getUserDataContainer(); if (!userDataContainer) continue; for (unsigned int i = 0; i < userDataContainer->getNumUserObjects(); ++i) { if (PtrHolder* p = dynamic_cast(userDataContainer->getUserObject(i))) - ptrHolder = p; + { + if (std::find(ignoreList.begin(), ignoreList.end(), p->mPtr) == ignoreList.end()) + { + ptrHolder = p; + } + } if (RefnumMarker* r = dynamic_cast(userDataContainer->getUserObject(i))) + { refnumMarkers.push_back(r); + } } } @@ -1058,21 +1095,113 @@ namespace MWRender || (intersectionIndex >= vertexCounter && intersectionIndex < vertexCounter + refnumMarkers[i]->mNumVertices)) { - result.mHitRefnum = refnumMarkers[i]->mRefnum; + auto it = std::find_if( + ignoreList.begin(), ignoreList.end(), [target = refnumMarkers[i]->mRefnum](const auto& ptr) { + return target == ptr.getCellRef().getRefNum(); + }); + + if (it == ignoreList.end()) + { + result.mHitRefnum = refnumMarkers[i]->mRefnum; + } + break; } vertexCounter += refnumMarkers[i]->mNumVertices; } + + if (!result.mHitObject.isEmpty() || result.mHitRefnum.isSet() || hitNonObjectWorld) + { + result.mHit = true; + result.mHitPointWorld = intersection.getWorldIntersectPoint(); + result.mHitNormalWorld = intersection.getWorldIntersectNormal(); + result.mRatio = intersection.ratio; + } + }; + + if (ignoreList.empty() || intersector->getIntersectionLimit() != osgUtil::LineSegmentIntersector::NO_LIMIT) + { + test(intersector->getFirstIntersection()); + } + else + { + for (const auto& intersection : intersector->getIntersections()) + { + test(intersection); + + if (result.mHit) + { + break; + } + } } return result; } + class IntersectionVisitorWithIgnoreList : public osgUtil::IntersectionVisitor + { + public: + bool skipTransform(osg::Transform& transform) + { + if (mContainsPagedRefs) + return false; + + osg::UserDataContainer* userDataContainer = transform.getUserDataContainer(); + if (!userDataContainer) + return false; + + for (unsigned int i = 0; i < userDataContainer->getNumUserObjects(); ++i) + { + if (PtrHolder* p = dynamic_cast(userDataContainer->getUserObject(i))) + { + if (std::find(mIgnoreList.begin(), mIgnoreList.end(), p->mPtr) != mIgnoreList.end()) + { + return true; + } + } + } + + return false; + } + + void apply(osg::Transform& transform) override + { + if (skipTransform(transform)) + { + return; + } + osgUtil::IntersectionVisitor::apply(transform); + } + + void setIgnoreList(std::span ignoreList) { mIgnoreList = ignoreList; } + void setContainsPagedRefs(bool contains) { mContainsPagedRefs = contains; } + + private: + std::span mIgnoreList; + bool mContainsPagedRefs = false; + }; + osg::ref_ptr RenderingManager::getIntersectionVisitor( - osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors) + osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors, + std::span ignoreList) { if (!mIntersectionVisitor) - mIntersectionVisitor = new osgUtil::IntersectionVisitor; + mIntersectionVisitor = new IntersectionVisitorWithIgnoreList; + + mIntersectionVisitor->setIgnoreList(ignoreList); + mIntersectionVisitor->setContainsPagedRefs(false); + + MWWorld::Scene* worldScene = MWBase::Environment::get().getWorldScene(); + for (const auto& ptr : ignoreList) + { + if (worldScene->isPagedRef(ptr)) + { + mIntersectionVisitor->setContainsPagedRefs(true); + intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); + break; + } + } mIntersectionVisitor->setTraversalNumber(mViewer->getFrameStamp()->getFrameNumber()); mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp()); @@ -1090,16 +1219,16 @@ namespace MWRender return mIntersectionVisitor; } - RenderingManager::RayResult RenderingManager::castRay( - const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors) + RenderingManager::RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, + bool ignorePlayer, bool ignoreActors, std::span ignoreList) { osg::ref_ptr intersector( new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL, origin, dest)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); - mRootNode->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); + mRootNode->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors, ignoreList)); - return getIntersectionResult(intersector); + return getIntersectionResult(intersector, mIntersectionVisitor, ignoreList); } RenderingManager::RayResult RenderingManager::castCameraToViewportRay( @@ -1119,7 +1248,7 @@ namespace MWRender mViewer->getCamera()->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); - return getIntersectionResult(intersector); + return getIntersectionResult(intersector, mIntersectionVisitor); } void RenderingManager::updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated) @@ -1128,7 +1257,7 @@ namespace MWRender mActorsPaths->updatePtr(old, updated); } - void RenderingManager::spawnEffect(const std::string& model, std::string_view texture, + void RenderingManager::spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX) { mEffectManager->addEffect(model, texture, worldPosition, scale, isMagicVFX); @@ -1238,8 +1367,8 @@ namespace MWRender if (mViewDistance < mNearClip) throw std::runtime_error("Viewing distance is less than near clip"); - double width = Settings::Manager::getInt("resolution x", "Video"); - double height = Settings::Manager::getInt("resolution y", "Video"); + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; double aspect = (height == 0.0) ? 1.0 : width / height; float fov = mFieldOfView; @@ -1264,7 +1393,7 @@ namespace MWRender mSharedUniformStateUpdater->setScreenRes(res.x(), res.y()); Stereo::Manager::instance().setMasterProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix()); } - else if (!mPostProcessor->isEnabled()) + else { mSharedUniformStateUpdater->setScreenRes(width, height); } @@ -1324,23 +1453,22 @@ namespace MWRender return existingChunkMgr->second; RenderingManager::WorldspaceChunkMgr newChunkMgr; - const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain"); - const bool groundcover = Settings::groundcover().mEnabled; - bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); + const float lodFactor = Settings::terrain().mLodFactor; + const bool groundcover = Settings::groundcover().mEnabled && worldspace == ESM::Cell::sDefaultWorldspaceId; + const bool distantTerrain = Settings::terrain().mDistantTerrain; + const double expiryDelay = Settings::cells().mCacheExpiryDelay; if (distantTerrain || groundcover) { - const int compMapResolution = Settings::Manager::getInt("composite map resolution", "Terrain"); - int compMapPower = Settings::Manager::getInt("composite map level", "Terrain"); - compMapPower = std::max(-3, compMapPower); - float compMapLevel = pow(2, compMapPower); - const int vertexLodMod = Settings::Manager::getInt("vertex lod mod", "Terrain"); - float maxCompGeometrySize = Settings::Manager::getFloat("max composite geometry size", "Terrain"); - maxCompGeometrySize = std::max(maxCompGeometrySize, 1.f); - bool debugChunks = Settings::Manager::getBool("debug chunks", "Terrain"); + const int compMapResolution = Settings::terrain().mCompositeMapResolution; + const int compMapPower = Settings::terrain().mCompositeMapLevel; + const float compMapLevel = std::pow(2, compMapPower); + const int vertexLodMod = Settings::terrain().mVertexLodMod; + const float maxCompGeometrySize = Settings::terrain().mMaxCompositeGeometrySize; + const bool debugChunks = Settings::terrain().mDebugChunks; auto quadTreeWorld = std::make_unique(mSceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, compMapResolution, compMapLevel, - lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace); - if (Settings::Manager::getBool("object paging", "Terrain")) + lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace, expiryDelay); + if (Settings::terrain().mObjectPaging) { newChunkMgr.mObjectPaging = std::make_unique(mResourceSystem->getSceneManager(), worldspace); @@ -1361,11 +1489,12 @@ namespace MWRender } else newChunkMgr.mTerrain = std::make_unique(mSceneRoot, mRootNode, mResourceSystem, - mTerrainStorage.get(), Mask_Terrain, worldspace, Mask_PreCompile, Mask_Debug); + mTerrainStorage.get(), Mask_Terrain, worldspace, expiryDelay, Mask_PreCompile, Mask_Debug); newChunkMgr.mTerrain->setTargetFrameRate(Settings::cells().mTargetFramerate); float distanceMult = std::cos(osg::DegreesToRadians(std::min(mFieldOfView, 140.f)) / 2.f); newChunkMgr.mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f / distanceMult : 1.f)); + newChunkMgr.mTerrain->enableHeightCullCallback(Settings::terrain().mWaterCulling); return mWorldspaceChunks.emplace(worldspace, std::move(newChunkMgr)).first->second; } @@ -1411,23 +1540,38 @@ namespace MWRender } else if (it->first == "Shaders" && it->second == "minimum interior brightness") { - mMinimumAmbientLuminance - = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); if (MWMechanics::getPlayer().isInCell()) configureAmbient(*MWMechanics::getPlayer().getCell()->getCell()); } + else if (it->first == "Shaders" + && (it->second == "force per pixel lighting" || it->second == "classic falloff")) + { + mViewer->stopThreading(); + + auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + defines["forcePPL"] = Settings::shaders().mForcePerPixelLighting ? "1" : "0"; + defines["classicFalloff"] = Settings::shaders().mClassicFalloff ? "1" : "0"; + mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); + + if (MWMechanics::getPlayer().isInCell() && it->second == "classic falloff") + configureAmbient(*MWMechanics::getPlayer().getCell()->getCell()); + + mViewer->startThreading(); + } else if (it->first == "Shaders" && (it->second == "light bounds multiplier" || it->second == "maximum light distance" || it->second == "light fade start" || it->second == "max lights")) { auto* lightManager = getLightRoot(); - lightManager->processChangedSettings(changed); + + lightManager->processChangedSettings(Settings::shaders().mLightBoundsMultiplier, + Settings::shaders().mMaximumLightDistance, Settings::shaders().mLightFadeStart); if (it->second == "max lights" && !lightManager->usingFFP()) { mViewer->stopThreading(); - lightManager->updateMaxLights(); + lightManager->updateMaxLights(Settings::shaders().mMaxLights); auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (const auto& [name, key] : lightManager->getLightDefines()) @@ -1441,7 +1585,7 @@ namespace MWRender } else if (it->first == "Post Processing" && it->second == "enabled") { - if (Settings::Manager::getBool("enabled", "Post Processing")) + if (Settings::postProcessing().mEnabled) mPostProcessor->enable(); else { @@ -1500,7 +1644,7 @@ namespace MWRender osg::Vec3f RenderingManager::getHalfExtents(const MWWorld::ConstPtr& object) const { osg::Vec3f halfExtents(0, 0, 0); - std::string modelName = object.getClass().getModel(object); + VFS::Path::Normalized modelName(object.getClass().getCorrectedModel(object)); if (modelName.empty()) return halfExtents; @@ -1522,24 +1666,39 @@ namespace MWRender osg::BoundingBox RenderingManager::getCullSafeBoundingBox(const MWWorld::Ptr& ptr) const { - const std::string model = ptr.getClass().getModel(ptr); - if (model.empty()) + if (ptr.isEmpty()) return {}; - osg::ref_ptr rootNode = new SceneUtil::PositionAttitudeTransform; - // Hack even used by osg internally, osg's NodeVisitor won't accept const qualified nodes - rootNode->addChild(const_cast(mResourceSystem->getSceneManager()->getTemplate(model).get())); + osg::ref_ptr rootNode = ptr.getRefData().getBaseNode(); - const float refScale = ptr.getCellRef().getScale(); - rootNode->setScale({ refScale, refScale, refScale }); - rootNode->setPosition(osg::Vec3(0, 0, 0)); - - osg::ref_ptr animation = nullptr; - - if (ptr.getClass().isNpc()) + // Recalculate bounds on the ptr's template when the object is not loaded or is loaded but paged + MWWorld::Scene* worldScene = MWBase::Environment::get().getWorldScene(); + if (!rootNode || worldScene->isPagedRef(ptr)) { - rootNode->setNodeMask(Mask_Actor); - animation = new NpcAnimation(ptr, osg::ref_ptr(rootNode), mResourceSystem); + const VFS::Path::Normalized model(ptr.getClass().getCorrectedModel(ptr)); + + if (model.empty()) + return {}; + + rootNode = new SceneUtil::PositionAttitudeTransform; + // Hack even used by osg internally, osg's NodeVisitor won't accept const qualified nodes + rootNode->addChild(const_cast(mResourceSystem->getSceneManager()->getTemplate(model).get())); + + const float refScale = ptr.getCellRef().getScale(); + rootNode->setScale({ refScale, refScale, refScale }); + const auto& rotation = ptr.getCellRef().getPosition().rot; + if (!ptr.getClass().isActor()) + rootNode->setAttitude(osg::Quat(rotation[0], osg::Vec3(-1, 0, 0)) + * osg::Quat(rotation[1], osg::Vec3(0, -1, 0)) * osg::Quat(rotation[2], osg::Vec3(0, 0, -1))); + rootNode->setPosition(ptr.getCellRef().getPosition().asVec3()); + + osg::ref_ptr animation = nullptr; + + if (ptr.getClass().isNpc()) + { + rootNode->setNodeMask(Mask_Actor); + animation = new NpcAnimation(ptr, osg::ref_ptr(rootNode), mResourceSystem); + } } SceneUtil::CullSafeBoundsVisitor computeBounds; @@ -1644,7 +1803,7 @@ namespace MWRender { if (!ptr.isInCell() || !ptr.getCell()->isExterior() || !mObjectPaging) return; - const ESM::RefNum& refnum = ptr.getCellRef().getRefNum(); + ESM::RefNum refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile()) return; if (mObjectPaging->blacklistObject(type, refnum, ptr.getCellRef().getPosition().asVec3(), @@ -1666,7 +1825,7 @@ namespace MWRender mObjectPaging->getPagedRefnums(activeGrid, out); } - void RenderingManager::setNavMeshMode(NavMeshMode value) + void RenderingManager::setNavMeshMode(Settings::NavMeshRenderMode value) { mNavMesh->setMode(value); } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 0e2ece7de9..7e68f666a1 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -1,21 +1,21 @@ #ifndef OPENMW_MWRENDER_RENDERINGMANAGER_H #define OPENMW_MWRENDER_RENDERINGMANAGER_H -#include -#include -#include - -#include - -#include - -#include "navmeshmode.hpp" #include "objects.hpp" #include "renderinginterface.hpp" #include "rendermode.hpp" +#include +#include + +#include +#include + +#include + #include #include +#include #include namespace osg @@ -88,6 +88,7 @@ namespace MWRender class StateUpdater; class SharedUniformStateUpdater; class PerViewUniformStateUpdater; + class IntersectionVisitorWithIgnoreList; class EffectManager; class ScreenshotManager; @@ -134,7 +135,6 @@ namespace MWRender void setAmbientColour(const osg::Vec4f& colour); - void skySetDate(int day, int month); int skyGetMasserPhase() const; int skyGetSecundaPhase() const; void skySetMoonColour(bool red); @@ -166,7 +166,6 @@ namespace MWRender /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h); - bool screenshot360(osg::Image* image); struct RayResult { @@ -178,8 +177,8 @@ namespace MWRender float mRatio; }; - RayResult castRay( - const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors = false); + RayResult castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, + bool ignoreActors = false, std::span ignoreList = {}); /// Return the object under the mouse cursor / crosshair position, given by nX and nY normalized screen /// coordinates, where (0,0) is the top left corner. @@ -196,7 +195,7 @@ namespace MWRender SkyManager* getSkyManager(); - void spawnEffect(const std::string& model, std::string_view texture, const osg::Vec3f& worldPosition, + void spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, const osg::Vec3f& worldPosition, float scale = 1.f, bool isMagicVFX = true); /// Clear all savegame-specific data @@ -275,13 +274,12 @@ namespace MWRender void setScreenRes(int width, int height); - void setNavMeshMode(NavMeshMode value); + void setNavMeshMode(Settings::NavMeshRenderMode value); private: void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); - void updateThirdPersonViewMode(); struct WorldspaceChunkMgr { @@ -300,10 +298,10 @@ namespace MWRender const bool mSkyBlending; - osg::ref_ptr getIntersectionVisitor( - osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors); + osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, + bool ignorePlayer, bool ignoreActors, std::span ignoreList = {}); - osg::ref_ptr mIntersectionVisitor; + osg::ref_ptr mIntersectionVisitor; osg::ref_ptr mViewer; osg::ref_ptr mRootNode; @@ -336,14 +334,13 @@ namespace MWRender osg::ref_ptr mPlayerAnimation; osg::ref_ptr mPlayerNode; std::unique_ptr mCamera; - std::unique_ptr mDebugDraw; + osg::ref_ptr mDebugDraw; osg::ref_ptr mStateUpdater; osg::ref_ptr mSharedUniformStateUpdater; osg::ref_ptr mPerViewUniformStateUpdater; osg::Vec4f mAmbientColor; - float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp index 191ff0e714..bb8248217a 100644 --- a/apps/openmw/mwrender/ripples.cpp +++ b/apps/openmw/mwrender/ripples.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "../mwworld/ptr.hpp" @@ -43,9 +44,9 @@ namespace MWRender mUseCompute = false; #else constexpr float minimumGLVersionRequiredForCompute = 4.4; - osg::GLExtensions* exts = osg::GLExtensions::Get(0, false); - mUseCompute = exts->glVersion >= minimumGLVersionRequiredForCompute - && exts->glslLanguageVersion >= minimumGLVersionRequiredForCompute; + osg::GLExtensions& exts = SceneUtil::getGLExtensions(); + mUseCompute = exts.glVersion >= minimumGLVersionRequiredForCompute + && exts.glslLanguageVersion >= minimumGLVersionRequiredForCompute; #endif if (mUseCompute) @@ -63,7 +64,7 @@ namespace MWRender stateset->addUniform(new osg::Uniform("offset", osg::Vec2f())); stateset->addUniform(new osg::Uniform("positionCount", 0)); stateset->addUniform(new osg::Uniform(osg::Uniform::Type::FLOAT_VEC3, "positions", 100)); - stateset->setAttributeAndModes(new osg::Viewport(0, 0, RipplesSurface::mRTTSize, RipplesSurface::mRTTSize)); + stateset->setAttributeAndModes(new osg::Viewport(0, 0, RipplesSurface::sRTTSize, RipplesSurface::sRTTSize)); mState[i].mStateset = stateset; } @@ -71,14 +72,13 @@ namespace MWRender { osg::ref_ptr texture = new osg::Texture2D; texture->setSourceFormat(GL_RGBA); - texture->setSourceType(GL_HALF_FLOAT); texture->setInternalFormat(GL_RGBA16F); texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture::LINEAR); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_BORDER); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_BORDER); texture->setBorderColor(osg::Vec4(0, 0, 0, 0)); - texture->setTextureSize(mRTTSize, mRTTSize); + texture->setTextureSize(sRTTSize, sRTTSize); mTextures[i] = texture; @@ -99,14 +99,14 @@ namespace MWRender { auto& shaderManager = mResourceSystem->getSceneManager()->getShaderManager(); - Shader::ShaderManager::DefineMap defineMap = { { "ripple_map_size", std::to_string(mRTTSize) + ".0" } }; + Shader::ShaderManager::DefineMap defineMap = { { "rippleMapSize", std::to_string(sRTTSize) + ".0" } }; osg::ref_ptr vertex = shaderManager.getShader("fullscreen_tri.vert", {}, osg::Shader::VERTEX); mProgramBlobber = shaderManager.getProgram( vertex, shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT)); mProgramSimulation = shaderManager.getProgram( - vertex, shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT)); + std::move(vertex), shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT)); } void RipplesSurface::setupComputePipeline() @@ -119,58 +119,83 @@ namespace MWRender nullptr, shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE)); } + void RipplesSurface::updateState(const osg::FrameStamp& frameStamp, State& state) + { + state.mPaused = mPaused; + + if (mPaused) + return; + + constexpr double updateFrequency = 60.0; + constexpr double updatePeriod = 1.0 / updateFrequency; + + const double simulationTime = frameStamp.getSimulationTime(); + const double frameDuration = simulationTime - mLastSimulationTime; + mLastSimulationTime = simulationTime; + + mRemainingWaveTime += frameDuration; + const double ticks = std::floor(mRemainingWaveTime * updateFrequency); + mRemainingWaveTime -= ticks * updatePeriod; + + if (ticks == 0) + { + state.mPaused = true; + return; + } + + const MWWorld::Ptr player = MWMechanics::getPlayer(); + const ESM::Position& playerPos = player.getRefData().getPosition(); + + mCurrentPlayerPos = osg::Vec2f( + std::floor(playerPos.pos[0] / sWorldScaleFactor), std::floor(playerPos.pos[1] / sWorldScaleFactor)); + const osg::Vec2f offset = mCurrentPlayerPos - mLastPlayerPos; + mLastPlayerPos = mCurrentPlayerPos; + + state.mStateset->getUniform("positionCount")->set(static_cast(mPositionCount)); + state.mStateset->getUniform("offset")->set(offset); + + osg::Uniform* const positions = state.mStateset->getUniform("positions"); + + for (std::size_t i = 0; i < mPositionCount; ++i) + { + osg::Vec3f pos = mPositions[i] + - osg::Vec3f(mCurrentPlayerPos.x() * sWorldScaleFactor, mCurrentPlayerPos.y() * sWorldScaleFactor, 0.0) + + osg::Vec3f(sRTTSize * sWorldScaleFactor / 2, sRTTSize * sWorldScaleFactor / 2, 0.0); + pos /= sWorldScaleFactor; + positions->setElement(i, pos); + } + positions->dirty(); + + mPositionCount = 0; + } + void RipplesSurface::traverse(osg::NodeVisitor& nv) { - if (!nv.getFrameStamp()) + const osg::FrameStamp* const frameStamp = nv.getFrameStamp(); + + if (frameStamp == nullptr) return; if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR) - { - size_t frameId = nv.getFrameStamp()->getFrameNumber() % 2; + updateState(*frameStamp, mState[frameStamp->getFrameNumber() % 2]); - const ESM::Position& player = MWMechanics::getPlayer().getRefData().getPosition(); - - mCurrentPlayerPos = osg::Vec2f( - std::floor(player.pos[0] / mWorldScaleFactor), std::floor(player.pos[1] / mWorldScaleFactor)); - osg::Vec2f offset = mCurrentPlayerPos - mLastPlayerPos; - mLastPlayerPos = mCurrentPlayerPos; - mState[frameId].mPaused = mPaused; - mState[frameId].mOffset = offset; - mState[frameId].mStateset->getUniform("positionCount")->set(static_cast(mPositionCount)); - mState[frameId].mStateset->getUniform("offset")->set(offset); - - auto* positions = mState[frameId].mStateset->getUniform("positions"); - - for (size_t i = 0; i < mPositionCount; ++i) - { - osg::Vec3f pos = mPositions[i] - - osg::Vec3f( - mCurrentPlayerPos.x() * mWorldScaleFactor, mCurrentPlayerPos.y() * mWorldScaleFactor, 0.0) - + osg::Vec3f(mRTTSize * mWorldScaleFactor / 2, mRTTSize * mWorldScaleFactor / 2, 0.0); - pos /= mWorldScaleFactor; - positions->setElement(i, pos); - } - positions->dirty(); - - mPositionCount = 0; - } osg::Geometry::traverse(nv); } void RipplesSurface::drawImplementation(osg::RenderInfo& renderInfo) const { osg::State& state = *renderInfo.getState(); - osg::GLExtensions& ext = *state.get(); - size_t contextID = state.getContextID(); - - size_t currentFrame = state.getFrameStamp()->getFrameNumber() % 2; + const std::size_t currentFrame = state.getFrameStamp()->getFrameNumber() % 2; const State& frameState = mState[currentFrame]; if (frameState.mPaused) { return; } - auto bindImage = [contextID, &state, &ext](osg::Texture2D* texture, GLuint index, GLenum access) { + osg::GLExtensions& ext = *state.get(); + const std::size_t contextID = state.getContextID(); + + const auto bindImage = [&](osg::Texture2D* texture, GLuint index, GLenum access) { osg::Texture::TextureObject* to = texture->getTextureObject(contextID); if (!to || texture->isDirty(contextID)) { @@ -180,53 +205,55 @@ namespace MWRender ext.glBindImageTexture(index, to->id(), 0, GL_FALSE, 0, access, GL_RGBA16F); }; - // Run simulation at a fixed rate independent on current FPS - // FIXME: when we skip frames we need to preserve positions. this doesn't work now - size_t ticks = 1; - // PASS: Blot in all ripple spawners - mProgramBlobber->apply(state); - state.apply(frameState.mStateset); - - for (size_t i = 0; i < ticks; i++) + state.pushStateSet(frameState.mStateset); + state.apply(); + state.applyAttribute(mProgramBlobber); + for (const auto& [name, stack] : state.getUniformMap()) { - if (mUseCompute) - { - bindImage(mTextures[1], 0, GL_WRITE_ONLY_ARB); - bindImage(mTextures[0], 1, GL_READ_ONLY_ARB); + if (!stack.uniformVec.empty()) + state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first)); + } - ext.glDispatchCompute(mRTTSize / 16, mRTTSize / 16, 1); - ext.glMemoryBarrier(GL_ALL_BARRIER_BITS); - } - else - { - mFBOs[1]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); - state.applyTextureAttribute(0, mTextures[0]); - osg::Geometry::drawImplementation(renderInfo); - } + if (mUseCompute) + { + bindImage(mTextures[1], 0, GL_WRITE_ONLY_ARB); + bindImage(mTextures[0], 1, GL_READ_ONLY_ARB); + + ext.glDispatchCompute(sRTTSize / 16, sRTTSize / 16, 1); + ext.glMemoryBarrier(GL_ALL_BARRIER_BITS); + } + else + { + mFBOs[1]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.applyTextureAttribute(0, mTextures[0]); + osg::Geometry::drawImplementation(renderInfo); } // PASS: Wave simulation - mProgramSimulation->apply(state); - state.apply(frameState.mStateset); - - for (size_t i = 0; i < ticks; i++) + state.applyAttribute(mProgramSimulation); + for (const auto& [name, stack] : state.getUniformMap()) { - if (mUseCompute) - { - bindImage(mTextures[0], 0, GL_WRITE_ONLY_ARB); - bindImage(mTextures[1], 1, GL_READ_ONLY_ARB); - - ext.glDispatchCompute(mRTTSize / 16, mRTTSize / 16, 1); - ext.glMemoryBarrier(GL_ALL_BARRIER_BITS); - } - else - { - mFBOs[0]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); - state.applyTextureAttribute(0, mTextures[1]); - osg::Geometry::drawImplementation(renderInfo); - } + if (!stack.uniformVec.empty()) + state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first)); } + + if (mUseCompute) + { + bindImage(mTextures[0], 0, GL_WRITE_ONLY_ARB); + bindImage(mTextures[1], 1, GL_READ_ONLY_ARB); + + ext.glDispatchCompute(sRTTSize / 16, sRTTSize / 16, 1); + ext.glMemoryBarrier(GL_ALL_BARRIER_BITS); + } + else + { + mFBOs[0]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.applyTextureAttribute(0, mTextures[1]); + osg::Geometry::drawImplementation(renderInfo); + } + + state.popStateSet(); } osg::Texture* RipplesSurface::getColorTexture() const @@ -270,7 +297,7 @@ namespace MWRender setReferenceFrame(osg::Camera::ABSOLUTE_RF); setNodeMask(Mask_RenderToTexture); setClearMask(GL_NONE); - setViewport(0, 0, RipplesSurface::mRTTSize, RipplesSurface::mRTTSize); + setViewport(0, 0, RipplesSurface::sRTTSize, RipplesSurface::sRTTSize); addChild(mRipples); setCullingActive(false); setImplicitBufferAttachmentMask(0, 0); diff --git a/apps/openmw/mwrender/ripples.hpp b/apps/openmw/mwrender/ripples.hpp index 0d5b055eb5..e355b16ecd 100644 --- a/apps/openmw/mwrender/ripples.hpp +++ b/apps/openmw/mwrender/ripples.hpp @@ -46,28 +46,30 @@ namespace MWRender void releaseGLObjects(osg::State* state) const override; - static constexpr size_t mRTTSize = 1024; + static constexpr size_t sRTTSize = 1024; // e.g. texel to cell unit ratio - static constexpr float mWorldScaleFactor = 2.5; - - Resource::ResourceSystem* mResourceSystem; + static constexpr float sWorldScaleFactor = 2.5; + private: struct State { - osg::Vec2f mOffset; - osg::ref_ptr mStateset; bool mPaused = true; + osg::ref_ptr mStateset; }; + void setupFragmentPipeline(); + + void setupComputePipeline(); + + inline void updateState(const osg::FrameStamp& frameStamp, State& state); + + Resource::ResourceSystem* mResourceSystem; + size_t mPositionCount = 0; std::array mPositions; std::array mState; - private: - void setupFragmentPipeline(); - void setupComputePipeline(); - osg::Vec2f mCurrentPlayerPos; osg::Vec2f mLastPlayerPos; @@ -79,6 +81,9 @@ namespace MWRender bool mPaused = false; bool mUseCompute = false; + + double mLastSimulationTime = 0; + double mRemainingWaveTime = 0; }; class Ripples : public osg::Camera diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index abfb7ba9cd..88a620efd0 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -41,8 +41,8 @@ namespace { std::ostringstream texname; texname << "textures/water/" << tex << std::setw(2) << std::setfill('0') << i << ".dds"; - osg::ref_ptr tex2( - new osg::Texture2D(resourceSystem->getImageManager()->getImage(texname.str()))); + const VFS::Path::Normalized path(texname.str()); + osg::ref_ptr tex2(new osg::Texture2D(resourceSystem->getImageManager()->getImage(path))); tex2->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); tex2->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); resourceSystem->getSceneManager()->applyFilterSettings(tex2); @@ -80,6 +80,25 @@ namespace node->setStateSet(stateset); } + + int findOldestParticleAlive(const osgParticle::ParticleSystem* partsys) + { + int oldest = -1; + float oldestAge = 0.f; + for (int i = 0; i < partsys->numParticles(); ++i) + { + const osgParticle::Particle* particle = partsys->getParticle(i); + if (!particle->isAlive()) + continue; + const float age = particle->getAge(); + if (oldest == -1 || age > oldestAge) + { + oldest = i; + oldestAge = age; + } + } + return oldest; + } } namespace MWRender @@ -87,6 +106,7 @@ namespace MWRender RippleSimulation::RippleSimulation(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : mParent(parent) + , mMaxNumberRipples(Fallback::Map::getInt("Water_MaxNumberRipples")) { mParticleSystem = new osgParticle::ParticleSystem; @@ -159,9 +179,6 @@ namespace MWRender currentPos.z() = mParticleNode->getPosition().z(); - if (mParticleSystem->numParticles() - mParticleSystem->numDeadParticles() > 500) - continue; // TODO: remove the oldest particle to make room? - emitRipple(currentPos); } } @@ -226,7 +243,19 @@ namespace MWRender } else { + if (mMaxNumberRipples <= 0) + return; + osgParticle::ParticleSystem::ScopedWriteLock lock(*mParticleSystem->getReadWriteMutex()); + if (mParticleSystem->numParticles() - mParticleSystem->numDeadParticles() > mMaxNumberRipples) + { + // osgParticle::ParticleSystem design requires this to be O(N) + // However, the number of particles we'll have to go through is not large + // If the user makes the limit absurd and manages to actually hit it this could be a problem + const int oldest = findOldestParticleAlive(mParticleSystem); + if (oldest != -1) + mParticleSystem->reuseParticle(oldest); + } osgParticle::Particle* p = mParticleSystem->createParticle(nullptr); p->setPosition(osg::Vec3f(pos.x(), pos.y(), 0.f)); p->setAngle(osg::Vec3f(0, 0, Misc::Rng::rollProbability() * osg::PI * 2 - osg::PI)); diff --git a/apps/openmw/mwrender/ripplesimulation.hpp b/apps/openmw/mwrender/ripplesimulation.hpp index 1f09797bf4..10b47f5679 100644 --- a/apps/openmw/mwrender/ripplesimulation.hpp +++ b/apps/openmw/mwrender/ripplesimulation.hpp @@ -74,6 +74,8 @@ namespace MWRender std::vector mEmitters; Ripples* mRipples = nullptr; + + int mMaxNumberRipples; }; } diff --git a/apps/openmw/mwrender/rotatecontroller.cpp b/apps/openmw/mwrender/rotatecontroller.cpp index d7f8bb902c..0e84087710 100644 --- a/apps/openmw/mwrender/rotatecontroller.cpp +++ b/apps/openmw/mwrender/rotatecontroller.cpp @@ -1,6 +1,7 @@ #include "rotatecontroller.hpp" #include +#include namespace MWRender { @@ -34,7 +35,16 @@ namespace MWRender return; } osg::Matrix matrix = node->getMatrix(); - osg::Quat worldOrient = getWorldOrientation(node); + + osg::Quat worldOrient; + osg::NodePathList nodepaths = node->getParentalNodePaths(mRelativeTo); + + if (!nodepaths.empty()) + { + osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); + worldOrient = worldMat.getRotate(); + } + osg::Quat worldOrientInverse = worldOrient.inverse(); osg::Quat orient = worldOrient * mRotate * worldOrientInverse * matrix.getRotate(); @@ -43,20 +53,17 @@ namespace MWRender node->setMatrix(matrix); + // If we are linked to a bone we must call setMatrixInSkeletonSpace + osgAnimation::Bone* b = dynamic_cast(node); + if (b) + { + osgAnimation::Bone* parent = b->getBoneParent(); + if (parent) + matrix *= parent->getMatrixInSkeletonSpace(); + + b->setMatrixInSkeletonSpace(matrix); + } + traverse(node, nv); } - - osg::Quat RotateController::getWorldOrientation(osg::Node* node) - { - // this could be optimized later, we just need the world orientation, not the full matrix - osg::NodePathList nodepaths = node->getParentalNodePaths(mRelativeTo); - osg::Quat worldOrient; - if (!nodepaths.empty()) - { - osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); - worldOrient = worldMat.getRotate(); - } - return worldOrient; - } - } diff --git a/apps/openmw/mwrender/rotatecontroller.hpp b/apps/openmw/mwrender/rotatecontroller.hpp index 87bf0adfe1..3d9f44c6a3 100644 --- a/apps/openmw/mwrender/rotatecontroller.hpp +++ b/apps/openmw/mwrender/rotatecontroller.hpp @@ -24,11 +24,15 @@ namespace MWRender void setOffset(const osg::Vec3f& offset); void setRotate(const osg::Quat& rotate); + const osg::Vec3f& getOffset() const { return mOffset; } + + const osg::Quat& getRotate() const { return mRotate; } + + osg::Node* getRelativeTo() const { return mRelativeTo; } + void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); protected: - osg::Quat getWorldOrientation(osg::Node* node); - bool mEnabled; osg::Vec3f mOffset; osg::Quat mRotate; diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index 336a321cf0..2c86a70f23 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -3,39 +3,16 @@ #include #include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include #include #include #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwgui/loadingscreen.hpp" +#include "../mwbase/world.hpp" #include "postprocessor.hpp" -#include "util.hpp" -#include "vismask.hpp" -#include "water.hpp" namespace MWRender { - enum Screenshot360Type - { - Spherical, - Cylindrical, - Planet, - RawCubemap - }; class NotifyDrawCompletedCallback : public osg::Camera::DrawCallback { @@ -102,24 +79,6 @@ namespace MWRender int width = screenW - leftPadding * 2; int height = screenH - topPadding * 2; - // Ensure we are reading from the resolved framebuffer and not the multisampled render buffer. Also ensure - // that the readbuffer is set correctly with rendeirng to FBO. glReadPixel() cannot read from multisampled - // targets - PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); - osg::GLExtensions* ext = osg::GLExtensions::Get(renderInfo.getContextID(), false); - - if (ext) - { - size_t frameId = renderInfo.getState()->getFrameStamp()->getFrameNumber() % 2; - osg::FrameBufferObject* fbo = nullptr; - - if (postProcessor && postProcessor->getFbo(PostProcessor::FBO_Primary, frameId)) - fbo = postProcessor->getFbo(PostProcessor::FBO_Primary, frameId); - - if (fbo) - fbo->apply(*renderInfo.getState(), osg::FrameBufferObject::READ_FRAMEBUFFER); - } - mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE); mImage->scaleImage(mWidth, mHeight, 1); } @@ -130,14 +89,9 @@ namespace MWRender osg::ref_ptr mImage; }; - ScreenshotManager::ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, - osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water) + ScreenshotManager::ScreenshotManager(osgViewer::Viewer* viewer) : mViewer(viewer) - , mRootNode(std::move(rootNode)) - , mSceneRoot(std::move(sceneRoot)) , mDrawCompleteCallback(new NotifyDrawCompletedCallback) - , mResourceSystem(resourceSystem) - , mWater(water) { } @@ -145,224 +99,25 @@ namespace MWRender void ScreenshotManager::screenshot(osg::Image* image, int w, int h) { - osg::Camera* camera = mViewer->getCamera(); + osg::Camera* camera = MWBase::Environment::get().getWorld()->getPostProcessor()->getHUDCamera(); osg::ref_ptr tempDrw = new osg::Drawable; tempDrw->setDrawCallback(new ReadImageFromFramebufferCallback(image, w, h)); tempDrw->setCullingActive(false); tempDrw->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", osg::StateSet::USE_RENDERBIN_DETAILS); // so its after all scene bins but before POST_RENDER gui camera camera->addChild(tempDrw); - traversalsAndWait(mViewer->getFrameStamp()->getFrameNumber()); + + // Ref https://gitlab.com/OpenMW/openmw/-/issues/6013 + mDrawCompleteCallback->reset(mViewer->getFrameStamp()->getFrameNumber()); + mViewer->getCamera()->setFinalDrawCallback(mDrawCompleteCallback); + mViewer->eventTraversal(); + mViewer->updateTraversal(); + mViewer->renderingTraversals(); + mDrawCompleteCallback->waitTillDone(); + // now that we've "used up" the current frame, get a fresh frame number for the next frame() following after the // screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); camera->removeChild(tempDrw); } - - bool ScreenshotManager::screenshot360(osg::Image* image) - { - int screenshotW = mViewer->getCamera()->getViewport()->width(); - int screenshotH = mViewer->getCamera()->getViewport()->height(); - Screenshot360Type screenshotMapping = Spherical; - - const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); - std::vector settingArgs; - Misc::StringUtils::split(settingStr, settingArgs); - - if (settingArgs.size() > 0) - { - std::string_view typeStrings[4] = { "spherical", "cylindrical", "planet", "cubemap" }; - bool found = false; - - for (int i = 0; i < 4; ++i) - { - if (settingArgs[0] == typeStrings[i]) - { - screenshotMapping = static_cast(i); - found = true; - break; - } - } - - if (!found) - { - Log(Debug::Warning) << "Wrong screenshot type: " << settingArgs[0] << "."; - return false; - } - } - - // planet mapping needs higher resolution - int cubeSize = screenshotMapping == Planet ? screenshotW : screenshotW / 2; - - if (settingArgs.size() > 1) - { - screenshotW = std::min(10000, Misc::StringUtils::toNumeric(settingArgs[1], 0)); - } - - if (settingArgs.size() > 2) - { - screenshotH = std::min(10000, Misc::StringUtils::toNumeric(settingArgs[2], 0)); - } - - if (settingArgs.size() > 3) - { - cubeSize = std::min(5000, Misc::StringUtils::toNumeric(settingArgs[3], 0)); - } - - bool rawCubemap = screenshotMapping == RawCubemap; - - if (rawCubemap) - screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row - else if (screenshotMapping == Planet) - screenshotH = screenshotW; // use square resolution for planet mapping - - std::vector> images; - images.reserve(6); - - for (int i = 0; i < 6; ++i) - images.push_back(new osg::Image); - - osg::Vec3 directions[6] - = { rawCubemap ? osg::Vec3(1, 0, 0) : osg::Vec3(0, 0, 1), osg::Vec3(0, 0, -1), osg::Vec3(-1, 0, 0), - rawCubemap ? osg::Vec3(0, 0, 1) : osg::Vec3(1, 0, 0), osg::Vec3(0, 1, 0), osg::Vec3(0, -1, 0) }; - - double rotations[] = { -osg::PI / 2.0, osg::PI / 2.0, osg::PI, 0, osg::PI / 2.0, osg::PI / 2.0 }; - - for (int i = 0; i < 6; ++i) // for each cubemap side - { - osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0, 0, -1), directions[i]); - - if (!rawCubemap) - transform *= osg::Matrixd::rotate(rotations[i], osg::Vec3(0, 0, -1)); - - osg::Image* sideImage = images[i].get(); - makeCubemapScreenshot(sideImage, cubeSize, cubeSize, transform); - - if (!rawCubemap) - sideImage->flipHorizontal(); - } - - if (rawCubemap) // for raw cubemap don't run on GPU, just merge the images - { - image->allocateImage( - cubeSize * 6, cubeSize, images[0]->r(), images[0]->getPixelFormat(), images[0]->getDataType()); - - for (int i = 0; i < 6; ++i) - osg::copyImage(images[i].get(), 0, 0, 0, images[i]->s(), images[i]->t(), images[i]->r(), image, - i * cubeSize, 0, 0); - - return true; - } - - // run on GPU now: - osg::ref_ptr cubeTexture(new osg::TextureCubeMap); - cubeTexture->setResizeNonPowerOfTwoHint(false); - - cubeTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::NEAREST); - cubeTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::NEAREST); - - cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - for (int i = 0; i < 6; ++i) - cubeTexture->setImage(i, images[i].get()); - - osg::ref_ptr screenshotCamera(new osg::Camera); - osg::ref_ptr quad(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0, 0, 0), 2.0))); - - osg::ref_ptr stateset = quad->getOrCreateStateSet(); - - Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - stateset->setAttributeAndModes(shaderMgr.getProgram("360"), osg::StateAttribute::ON); - - stateset->addUniform(new osg::Uniform("cubeMap", 0)); - stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); - stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); - - screenshotCamera->addChild(quad); - - renderCameraToImage(screenshotCamera, image, screenshotW, screenshotH); - - return true; - } - - void ScreenshotManager::traversalsAndWait(unsigned int frame) - { - // Ref https://gitlab.com/OpenMW/openmw/-/issues/6013 - mDrawCompleteCallback->reset(frame); - mViewer->getCamera()->setFinalDrawCallback(mDrawCompleteCallback); - - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); - mDrawCompleteCallback->waitTillDone(); - } - - void ScreenshotManager::renderCameraToImage(osg::Camera* camera, osg::Image* image, int w, int h) - { - camera->setNodeMask(Mask_RenderToTexture); - camera->attach(osg::Camera::COLOR_BUFFER, image); - camera->setRenderOrder(osg::Camera::PRE_RENDER); - camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); - camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); - camera->setViewport(0, 0, w, h); - - SceneUtil::setCameraClearDepth(camera); - - osg::ref_ptr texture(new osg::Texture2D); - texture->setInternalFormat(GL_RGB); - texture->setTextureSize(w, h); - texture->setResizeNonPowerOfTwoHint(false); - texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - camera->attach(osg::Camera::COLOR_BUFFER, texture); - - image->setDataType(GL_UNSIGNED_BYTE); - image->setPixelFormat(texture->getInternalFormat()); - - mRootNode->addChild(camera); - - MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOn(false); - - // The draw needs to complete before we can copy back our image. - traversalsAndWait(0); - - MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOff(); - - // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the - // screenshot is completed - mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); - - camera->removeChildren(0, camera->getNumChildren()); - mRootNode->removeChild(camera); - } - - void ScreenshotManager::makeCubemapScreenshot(osg::Image* image, int w, int h, const osg::Matrixd& cameraTransform) - { - osg::ref_ptr rttCamera(new osg::Camera); - const float nearClip = Settings::camera().mNearClip; - const float viewDistance = Settings::camera().mViewingDistance; - // each cubemap side sees 90 degrees - if (SceneUtil::AutoDepth::isReversed()) - rttCamera->setProjectionMatrix( - SceneUtil::getReversedZProjectionMatrixAsPerspectiveInf(90.0, w / float(h), nearClip)); - else - rttCamera->setProjectionMatrixAsPerspective(90.0, w / float(h), nearClip, viewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); - - rttCamera->setUpdateCallback(new NoTraverseCallback); - rttCamera->addChild(mSceneRoot); - - rttCamera->addChild(mWater->getReflectionNode()); - rttCamera->addChild(mWater->getRefractionNode()); - - rttCamera->setCullMask( - MWBase::Environment::get().getWindowManager()->getCullMask() & ~(Mask_GUI | Mask_FirstPerson)); - - rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - - renderCameraToImage(rttCamera.get(), image, w, h); - } } diff --git a/apps/openmw/mwrender/screenshotmanager.hpp b/apps/openmw/mwrender/screenshotmanager.hpp index 72e5b91637..f41ccf0045 100644 --- a/apps/openmw/mwrender/screenshotmanager.hpp +++ b/apps/openmw/mwrender/screenshotmanager.hpp @@ -1,45 +1,25 @@ #ifndef MWRENDER_SCREENSHOTMANAGER_H #define MWRENDER_SCREENSHOTMANAGER_H -#include - -#include #include #include -namespace Resource -{ - class ResourceSystem; -} - namespace MWRender { - class Water; class NotifyDrawCompletedCallback; class ScreenshotManager { public: - ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, - osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water); + ScreenshotManager(osgViewer::Viewer* viewer); ~ScreenshotManager(); void screenshot(osg::Image* image, int w, int h); - bool screenshot360(osg::Image* image); private: osg::ref_ptr mViewer; - osg::ref_ptr mRootNode; - osg::ref_ptr mSceneRoot; osg::ref_ptr mDrawCompleteCallback; - Resource::ResourceSystem* mResourceSystem; - Water* mWater; - - void traversalsAndWait(unsigned int frame); - void renderCameraToImage(osg::Camera* camera, osg::Image* image, int w, int h); - void makeCubemapScreenshot( - osg::Image* image, int w, int h, const osg::Matrixd& cameraTransform = osg::Matrixd()); }; } diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index d2f725c4b8..1cc8cc0d3e 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -35,6 +35,7 @@ #include "renderbin.hpp" #include "skyutil.hpp" +#include "util.hpp" #include "vismask.hpp" namespace @@ -205,7 +206,8 @@ namespace { public: SkyRTT(osg::Vec2f size, osg::Group* earlyRenderBinRoot) - : RTTNode(static_cast(size.x()), static_cast(size.y()), 0, false, 1, StereoAwareness::Aware) + : RTTNode(static_cast(size.x()), static_cast(size.y()), 0, false, 1, StereoAwareness::Aware, + MWRender::shouldAddMSAAIntermediateTarget()) , mEarlyRenderBinRoot(earlyRenderBinRoot) { setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); @@ -218,7 +220,7 @@ namespace camera->setNodeMask(MWRender::Mask_RenderToTexture); camera->setCullMask(MWRender::Mask_Sky); camera->addChild(mEarlyRenderBinRoot); - SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); } private: @@ -236,10 +238,8 @@ namespace MWRender , mAtmosphereNightRoll(0.f) , mCreated(false) , mIsStorm(false) - , mDay(0) - , mMonth(0) + , mTimescaleClouds(Fallback::Map::getBool("Weather_Timescale_Clouds")) , mCloudAnimationTimer(0.f) - , mRainTimer(0.f) , mStormParticleDirection(MWWorld::Weather::defaultDirection()) , mStormDirection(MWWorld::Weather::defaultDirection()) , mClouds() @@ -247,18 +247,17 @@ namespace MWRender , mCloudBlendFactor(0.f) , mCloudSpeed(0.f) , mStarsOpacity(0.f) - , mRemainingTransitionTime(0.f) - , mRainEnabled(false) , mRainSpeed(0.f) , mRainDiameter(0.f) , mRainMinHeight(0.f) , mRainMaxHeight(0.f) , mRainEntranceSpeed(1.f) , mRainMaxRaindrops(0) + , mRainRipplesEnabled(Fallback::Map::getBool("Weather_Rain_Ripples")) + , mSnowRipplesEnabled(Fallback::Map::getBool("Weather_Snow_Ripples")) , mWindSpeed(0.f) , mBaseWindSpeed(0.f) , mEnabled(true) - , mSunEnabled(true) , mSunglareEnabled(true) , mPrecipitationAlpha(0.f) , mDirtyParticlesEffect(false) @@ -269,7 +268,8 @@ namespace MWRender if (!mSceneManager->getForceShaders()) skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED | osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); + mSceneManager->setUpNormalsRTForStateSet(skyroot->getOrCreateStateSet(), false); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*skyroot->getOrCreateStateSet()); parentNode->addChild(skyroot); mEarlyRenderBinRoot = new osg::Group; @@ -292,7 +292,7 @@ namespace MWRender mRootNode->addChild(mEarlyRenderBinRoot); mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); - mPrecipitationOcclusion = Settings::Manager::getBool("weather particle occlusion", "Shaders"); + mPrecipitationOcclusion = Settings::shaders().mWeatherParticleOcclusion; mPrecipitationOccluder = std::make_unique(skyroot, parentNode, rootNode, camera); } @@ -302,8 +302,7 @@ namespace MWRender bool forceShaders = mSceneManager->getForceShaders(); - mAtmosphereDay - = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); + mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere.get(), mEarlyRenderBinRoot); ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); mAtmosphereDay->accept(modAtmosphere); @@ -315,12 +314,10 @@ namespace MWRender mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); osg::ref_ptr atmosphereNight; - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - atmosphereNight = mSceneManager->getInstance( - Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); + if (mSceneManager->getVFS()->exists(Settings::models().mSkynight02.get())) + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight02.get(), mAtmosphereNightNode); else - atmosphereNight = mSceneManager->getInstance( - Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight01.get(), mAtmosphereNightNode); atmosphereNight->getOrCreateStateSet()->setAttributeAndModes( createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); @@ -341,7 +338,7 @@ namespace MWRender mCloudMesh = new osg::PositionAttitudeTransform; osg::ref_ptr cloudMeshChild - = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudMesh); + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mCloudMesh); mCloudUpdater = new CloudUpdater(forceShaders); mCloudUpdater->setOpacity(1.f); cloudMeshChild->addUpdateCallback(mCloudUpdater); @@ -349,7 +346,7 @@ namespace MWRender mNextCloudMesh = new osg::PositionAttitudeTransform; osg::ref_ptr nextCloudMeshChild - = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mNextCloudMesh); + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mNextCloudMesh); mNextCloudUpdater = new CloudUpdater(forceShaders); mNextCloudUpdater->setOpacity(0.f); nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); @@ -400,8 +397,9 @@ namespace MWRender osg::ref_ptr stateset = mRainParticleSystem->getOrCreateStateSet(); + constexpr VFS::Path::NormalizedView raindropImage("textures/tx_raindrop_01.dds"); osg::ref_ptr raindropTex - = new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds")); + = new osg::Texture2D(mSceneManager->getImageManager()->getImage(raindropImage)); raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); @@ -518,6 +516,20 @@ namespace MWRender return mRainNode != nullptr; } + bool SkyManager::getRainRipplesEnabled() const + { + if (!mEnabled || mIsStorm) + return false; + + if (hasRain()) + return mRainRipplesEnabled; + + if (mParticleNode && mCurrentParticleEffect == Settings::models().mWeathersnow.get()) + return mSnowRipplesEnabled; + + return false; + } + float SkyManager::getPrecipitationAlpha() const { if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) @@ -538,13 +550,22 @@ namespace MWRender osg::Quat quat; quat.makeRotate(MWWorld::Weather::defaultDirection(), mStormParticleDirection); // Morrowind deliberately rotates the blizzard mesh, so so should we. - if (mCurrentParticleEffect == "meshes\\blizzard.nif") + if (mCurrentParticleEffect == Settings::models().mWeatherblizzard.get()) quat.makeRotate(osg::Vec3f(-1, 0, 0), mStormParticleDirection); mParticleNode->setAttitude(quat); } + const float timeScale = MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale(); + // UV Scroll the clouds - mCloudAnimationTimer += duration * mCloudSpeed * 0.003; + float cloudDelta = duration * mCloudSpeed / 400.f; + if (mTimescaleClouds) + cloudDelta *= timeScale / 60.f; + + mCloudAnimationTimer += cloudDelta; + if (mCloudAnimationTimer >= 4.f) + mCloudAnimationTimer -= 4.f; + mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); mCloudUpdater->setTextureCoord(mCloudAnimationTimer); @@ -560,8 +581,7 @@ namespace MWRender } // rotate the stars by 360 degrees every 4 days - mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale() * duration - * osg::DegreesToRadians(360.f) / (3600 * 96.f); + mAtmosphereNightRoll += timeScale * duration * osg::DegreesToRadians(360.f) / (3600 * 96.f); if (mAtmosphereNightNode->getNodeMask() != 0) mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0, 0, 1))); mPrecipitationOccluder->update(); @@ -702,7 +722,7 @@ namespace MWRender const osg::Vec3 defaultWrapRange = osg::Vec3(1024, 1024, 800); const bool occlusionEnabledForEffect - = !mRainEffect.empty() || mCurrentParticleEffect == "meshes\\snow.nif"; + = !mRainEffect.empty() || mCurrentParticleEffect == Settings::models().mWeathersnow.get(); for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) { @@ -743,14 +763,15 @@ namespace MWRender { mClouds = weather.mCloudTexture; - std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + const VFS::Path::Normalized texture + = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mCloudUpdater->setTexture(cloudTex); + mCloudUpdater->setTexture(std::move(cloudTex)); } if (mStormDirection != weather.mStormDirection) @@ -765,14 +786,15 @@ namespace MWRender if (!mNextClouds.empty()) { - std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + const VFS::Path::Normalized texture + = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mNextCloudUpdater->setTexture(cloudTex); + mNextCloudUpdater->setTexture(std::move(cloudTex)); mNextStormDirection = weather.mStormDirection; } } @@ -893,12 +915,6 @@ namespace MWRender mSecunda->setState(state); } - void SkyManager::setDate(int day, int month) - { - mDay = day; - mMonth = month; - } - void SkyManager::setGlareTimeOfDayFade(float val) { mSun->setGlareTimeOfDayFade(val); @@ -909,18 +925,19 @@ namespace MWRender mUnderwaterSwitch->setWaterLevel(height); } - void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) + void SkyManager::listAssetsToPreload( + std::vector& models, std::vector& textures) { - models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - models.emplace_back(Settings::Manager::getString("skynight02", "Models")); - models.emplace_back(Settings::Manager::getString("skynight01", "Models")); - models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); + models.push_back(Settings::models().mSkyatmosphere); + if (mSceneManager->getVFS()->exists(Settings::models().mSkynight02.get())) + models.push_back(Settings::models().mSkynight02); + models.push_back(Settings::models().mSkynight01); + models.push_back(Settings::models().mSkyclouds); - models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); + models.push_back(Settings::models().mWeatherashcloud); + models.push_back(Settings::models().mWeatherblightcloud); + models.push_back(Settings::models().mWeathersnow); + models.push_back(Settings::models().mWeatherblizzard); textures.emplace_back("textures/tx_mooncircle_full_s.dds"); textures.emplace_back("textures/tx_mooncircle_full_m.dds"); diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index 75c6a10a50..6a32978c4e 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -8,6 +8,8 @@ #include #include +#include + #include "precipitationocclusion.hpp" #include "skyutil.hpp" @@ -52,12 +54,6 @@ namespace MWRender void setEnabled(bool enabled); - void setHour(double hour); - ///< will be called even when sky is disabled. - - void setDate(int day, int month); - ///< will be called even when sky is disabled. - int getMasserPhase() const; ///< 0 new moon, 1 waxing or waning cresecent, 2 waxing or waning half, /// 3 waxing or waning gibbous, 4 full moon @@ -79,9 +75,9 @@ namespace MWRender bool hasRain() const; - float getPrecipitationAlpha() const; + bool getRainRipplesEnabled() const; - void setRainSpeed(float speed); + float getPrecipitationAlpha() const; void setStormParticleDirection(const osg::Vec3f& direction); @@ -98,7 +94,8 @@ namespace MWRender /// Set height of water plane (used to remove underwater weather particles) void setWaterHeight(float height); - void listAssetsToPreload(std::vector& models, std::vector& textures); + void listAssetsToPreload( + std::vector& models, std::vector& textures); float getBaseWindSpeed() const; @@ -160,13 +157,9 @@ namespace MWRender bool mIsStorm; - int mDay; - int mMonth; - + bool mTimescaleClouds; float mCloudAnimationTimer; - float mRainTimer; - // particle system rotation is independent of cloud rotation internally osg::Vec3f mStormParticleDirection; osg::Vec3f mStormDirection; @@ -182,11 +175,8 @@ namespace MWRender osg::Vec4f mSkyColour; osg::Vec4f mFogColour; - std::string mCurrentParticleEffect; + VFS::Path::Normalized mCurrentParticleEffect; - float mRemainingTransitionTime; - - bool mRainEnabled; std::string mRainEffect; float mRainSpeed; float mRainDiameter; @@ -194,11 +184,12 @@ namespace MWRender float mRainMaxHeight; float mRainEntranceSpeed; int mRainMaxRaindrops; + bool mRainRipplesEnabled; + bool mSnowRipplesEnabled; float mWindSpeed; float mBaseWindSpeed; bool mEnabled; - bool mSunEnabled; bool mSunglareEnabled; float mPrecipitationAlpha; diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp index 3274b8c6b0..dacf628a2c 100644 --- a/apps/openmw/mwrender/skyutil.cpp +++ b/apps/openmw/mwrender/skyutil.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -29,6 +28,7 @@ #include #include +#include #include @@ -406,7 +406,7 @@ namespace MWRender } } - void setTextures(const std::string& phaseTex, const std::string& circleTex) + void setTextures(VFS::Path::NormalizedView phaseTex, VFS::Path::NormalizedView circleTex) { mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); @@ -766,12 +766,15 @@ namespace MWRender Resource::ImageManager& imageManager = *sceneManager.getImageManager(); - osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds")); + constexpr VFS::Path::NormalizedView image("textures/tx_sun_05.dds"); + + osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage(image)); sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - sunTex->setName("diffuseMap"); mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex); + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes( + 0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::ON); mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); osg::ref_ptr queryNode = new osg::Group; @@ -788,14 +791,14 @@ namespace MWRender stateset->setAttributeAndModes(alphaFunc); } stateset->setTextureAttributeAndModes(0, sunTex); + stateset->setTextureAttributeAndModes(0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::ON); stateset->setAttributeAndModes(createUnlitMaterial()); stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query))); // Disable writing to the color buffer. We are using this geometry for visibility tests only. osg::ref_ptr colormask = new osg::ColorMask(0, 0, 0, 0); stateset->setAttributeAndModes(colormask); - if (sceneManager.getSupportsNormalsRT()) - stateset->setAttributeAndModes(new osg::ColorMaski(1, false, false, false, false)); + sceneManager.setUpNormalsRTForStateSet(stateset, false); mTransform->addChild(queryNode); mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); @@ -882,12 +885,11 @@ namespace MWRender osg::StateSet* queryStateSet = new osg::StateSet; if (queryVisible) { - osg::ref_ptr depth = new SceneUtil::AutoDepth(osg::Depth::LEQUAL); + osg::ref_ptr depth = new SceneUtil::AutoDepth; // This is a trick to make fragments written by the query always use the maximum depth value, // without having to retrieve the current far clipping distance. // We want the sun glare to be "infinitely" far away. double far = SceneUtil::AutoDepth::isReversed() ? 0.0 : 1.0; - depth->setFunction(osg::Depth::LEQUAL); depth->setZNear(far); depth->setZFar(far); depth->setWriteMask(false); @@ -906,11 +908,10 @@ namespace MWRender void Sun::createSunFlash(Resource::ImageManager& imageManager) { - osg::ref_ptr tex - = new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds")); + constexpr VFS::Path::NormalizedView image("textures/tx_sun_flash_grey_05.dds"); + osg::ref_ptr tex = new osg::Texture2D(imageManager.getImage(image)); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - tex->setName("diffuseMap"); osg::ref_ptr group(new osg::Group); @@ -923,6 +924,7 @@ namespace MWRender osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setTextureAttributeAndModes(0, tex); + stateset->setTextureAttributeAndModes(0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::ON); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); stateset->setNestRenderBins(false); @@ -1114,10 +1116,18 @@ namespace MWRender textureName += ".dds"; + const VFS::Path::Normalized texturePath(std::move(textureName)); + if (mType == Moon::Type_Secunda) - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); + { + constexpr VFS::Path::NormalizedView secunda("textures/tx_mooncircle_full_s.dds"); + mUpdater->setTextures(texturePath, secunda); + } else - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); + { + constexpr VFS::Path::NormalizedView masser("textures/tx_mooncircle_full_m.dds"); + mUpdater->setTextures(texturePath, masser); + } } int RainCounter::numParticlesToCreate(double dt) const diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp index 1018724595..da038e6c58 100644 --- a/apps/openmw/mwrender/skyutil.hpp +++ b/apps/openmw/mwrender/skyutil.hpp @@ -65,6 +65,7 @@ namespace MWRender bool mIsStorm; ESM::RefId mAmbientLoopSoundID; + ESM::RefId mRainLoopSoundID; float mAmbientSoundVolume; std::string mParticleEffect; diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp index 11fdbd774f..9776d7e632 100644 --- a/apps/openmw/mwrender/terrainstorage.cpp +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -1,6 +1,7 @@ #include "terrainstorage.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" @@ -10,8 +11,8 @@ namespace MWRender { - TerrainStorage::TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern, - const std::string& normalHeightMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, + TerrainStorage::TerrainStorage(Resource::ResourceSystem* resourceSystem, std::string_view normalMapPattern, + std::string_view normalHeightMapPattern, bool autoUseNormalMaps, std::string_view specularMapPattern, bool autoUseSpecularMaps) : ESMTerrain::Storage(resourceSystem->getVFS(), normalMapPattern, normalHeightMapPattern, autoUseNormalMaps, specularMapPattern, autoUseSpecularMaps) @@ -33,6 +34,10 @@ namespace MWRender if (ESM::isEsm4Ext(cellLocation.mWorldspace)) { + const ESM4::World* worldspace = esmStore.get().find(cellLocation.mWorldspace); + if (!worldspace->mParent.isZeroOrUnset() && worldspace->mParentUseFlags & ESM4::World::UseFlag_Land) + cellLocation.mWorldspace = worldspace->mParent; + return esmStore.get().search(cellLocation) != nullptr; } else @@ -64,6 +69,10 @@ namespace MWRender if (ESM::isEsm4Ext(worldspace)) { + const ESM4::World* worldRec = esmStore.get().find(worldspace); + if (!worldRec->mParent.isZeroOrUnset() && worldRec->mParentUseFlags & ESM4::World::UseFlag_Land) + worldspace = worldRec->mParent; + const auto& lands = esmStore.get().getLands(); for (const auto& [landPos, _] : lands) { @@ -96,7 +105,7 @@ namespace MWRender return mLandManager->getLand(cellLocation); } - const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin) { const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); return esmStore.get().search(index, plugin); diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index 0b41c06428..731f396713 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -16,13 +16,13 @@ namespace MWRender class TerrainStorage : public ESMTerrain::Storage { public: - TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern = "", - const std::string& normalHeightMapPattern = "", bool autoUseNormalMaps = false, - const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false); + TerrainStorage(Resource::ResourceSystem* resourceSystem, std::string_view normalMapPattern = {}, + std::string_view normalHeightMapPattern = {}, bool autoUseNormalMaps = false, + std::string_view specularMapPattern = {}, bool autoUseSpecularMaps = false); ~TerrainStorage(); osg::ref_ptr getLand(ESM::ExteriorCellLocation cellLocation) override; - const ESM::LandTexture* getLandTexture(int index, short plugin) override; + const std::string* getLandTexture(std::uint16_t index, int plugin) override; bool hasData(ESM::ExteriorCellLocation cellLocation) override; diff --git a/apps/openmw/mwrender/util.cpp b/apps/openmw/mwrender/util.cpp index 509ea2efa7..c2231c31f8 100644 --- a/apps/openmw/mwrender/util.cpp +++ b/apps/openmw/mwrender/util.cpp @@ -6,65 +6,72 @@ #include #include #include +#include #include +#include namespace MWRender { - - class TextureOverrideVisitor : public osg::NodeVisitor + namespace { - public: - TextureOverrideVisitor(std::string_view texture, Resource::ResourceSystem* resourcesystem) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mTexture(texture) - , mResourcesystem(resourcesystem) + class TextureOverrideVisitor : public osg::NodeVisitor { - } - - void apply(osg::Node& node) override - { - int index = 0; - osg::ref_ptr nodePtr(&node); - if (node.getUserValue("overrideFx", index)) + public: + TextureOverrideVisitor(std::string_view texture, Resource::ResourceSystem* resourcesystem) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mTexture(texture) + , mResourcesystem(resourcesystem) { - if (index == 1) - overrideTexture(mTexture, mResourcesystem, std::move(nodePtr)); } - traverse(node); - } - std::string_view mTexture; - Resource::ResourceSystem* mResourcesystem; - }; - void overrideFirstRootTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node) - { - TextureOverrideVisitor overrideVisitor(texture, resourceSystem); - node->accept(overrideVisitor); + void apply(osg::Node& node) override + { + int index = 0; + if (node.getUserValue("overrideFx", index)) + { + if (index == 1) + overrideTexture(mTexture, mResourcesystem, node); + } + traverse(node); + } + std::string_view mTexture; + Resource::ResourceSystem* mResourcesystem; + }; } - void overrideTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node) + void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) + { + TextureOverrideVisitor overrideVisitor(texture, resourceSystem); + node.accept(overrideVisitor); + } + + void overrideTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) { if (texture.empty()) return; - std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(texture, resourceSystem->getVFS()); + const VFS::Path::Normalized correctedTexture + = Misc::ResourceHelpers::correctTexturePath(texture, resourceSystem->getVFS()); // Not sure if wrap settings should be pulled from the overridden texture? osg::ref_ptr tex = new osg::Texture2D(resourceSystem->getImageManager()->getImage(correctedTexture)); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - tex->setName("diffuseMap"); osg::ref_ptr stateset; - if (node->getStateSet()) - stateset = new osg::StateSet(*node->getStateSet(), osg::CopyOp::SHALLOW_COPY); + if (const osg::StateSet* const src = node.getStateSet()) + stateset = new osg::StateSet(*src, osg::CopyOp::SHALLOW_COPY); else stateset = new osg::StateSet; stateset->setTextureAttribute(0, tex, osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::OVERRIDE); - node->setStateSet(stateset); + node.setStateSet(stateset); + } + + bool shouldAddMSAAIntermediateTarget() + { + return Settings::shaders().mAntialiasAlphaTest && Settings::video().mAntialiasing > 1; } } diff --git a/apps/openmw/mwrender/util.hpp b/apps/openmw/mwrender/util.hpp index 457b23f94b..fc43680d67 100644 --- a/apps/openmw/mwrender/util.hpp +++ b/apps/openmw/mwrender/util.hpp @@ -2,8 +2,8 @@ #define OPENMW_MWRENDER_UTIL_H #include -#include -#include + +#include namespace osg { @@ -20,11 +20,9 @@ namespace MWRender // Overrides the texture of nodes in the mesh that had the same NiTexturingProperty as the first NiTexturingProperty // of the .NIF file's root node, if it had a NiTexturingProperty. Used for applying "particle textures" to magic // effects. - void overrideFirstRootTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node); + void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node); - void overrideTexture( - std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node); + void overrideTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node); // Node callback to entirely skip the traversal. class NoTraverseCallback : public osg::NodeCallback @@ -35,6 +33,8 @@ namespace MWRender // no traverse() } }; + + bool shouldAddMSAAIntermediateTarget(); } #endif diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 1b25dfc366..05ed534d29 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -59,8 +59,8 @@ namespace MWRender }; // Defines masks to remove when using ToggleWorld command - constexpr static unsigned int sToggleWorldMask - = Mask_Debug | Mask_Actor | Mask_Terrain | Mask_Object | Mask_Static | Mask_Groundcover; + constexpr inline unsigned int sToggleWorldMask + = Mask_Actor | Mask_Terrain | Mask_Object | Mask_Static | Mask_Groundcover; } diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 51ce06069c..81e44248ac 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -35,11 +35,14 @@ #include +#include + #include "../mwworld/cellstore.hpp" #include "renderbin.hpp" #include "ripples.hpp" #include "ripplesimulation.hpp" +#include "util.hpp" #include "vismask.hpp" namespace MWRender @@ -111,7 +114,10 @@ namespace MWRender } // move the plane back along its normal a little bit to prevent bleeding at the water shore - const float clipFudge = -5; + float fov = Settings::camera().mFieldOfView; + const float clipFudgeMin = 2.5; // minimum offset of clip plane + const float clipFudgeScale = -15000.0; + float clipFudge = abs(abs((*mCullPlane)[3]) - eyePoint.z()) * fov / clipFudgeScale - clipFudgeMin; modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * clipFudge); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); @@ -202,21 +208,25 @@ namespace MWRender } }; - class RainIntensityUpdater : public SceneUtil::StateSetUpdater + class RainSettingsUpdater : public SceneUtil::StateSetUpdater { public: - RainIntensityUpdater() + RainSettingsUpdater() : mRainIntensity(0.f) + , mEnableRipples(false) { } void setRainIntensity(float rainIntensity) { mRainIntensity = rainIntensity; } + void setRipplesEnabled(bool enableRipples) { mEnableRipples = enableRipples; } protected: void setDefaults(osg::StateSet* stateset) override { osg::ref_ptr rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f); stateset->addUniform(rainIntensityUniform.get()); + osg::ref_ptr enableRainRipplesUniform = new osg::Uniform("enableRainRipples", false); + stateset->addUniform(enableRainRipplesUniform.get()); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override @@ -224,17 +234,21 @@ namespace MWRender osg::ref_ptr rainIntensityUniform = stateset->getUniform("rainIntensity"); if (rainIntensityUniform != nullptr) rainIntensityUniform->set(mRainIntensity); + osg::ref_ptr enableRainRipplesUniform = stateset->getUniform("enableRainRipples"); + if (enableRainRipplesUniform != nullptr) + enableRainRipplesUniform->set(mEnableRipples); } private: float mRainIntensity; + bool mEnableRipples; }; class Refraction : public SceneUtil::RTTNode { public: Refraction(uint32_t rttSize) - : RTTNode(rttSize, rttSize, 0, false, 1, StereoAwareness::Aware) + : RTTNode(rttSize, rttSize, 0, false, 1, StereoAwareness::Aware, shouldAddMSAAIntermediateTarget()) , mNodeMask(Refraction::sDefaultCullMask) { setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); @@ -244,8 +258,7 @@ namespace MWRender void setDefaults(osg::Camera* camera) override { camera->setReferenceFrame(osg::Camera::RELATIVE_RF); - camera->setSmallFeatureCullingPixelSize( - Settings::Manager::getInt("small feature culling pixel size", "Water")); + camera->setSmallFeatureCullingPixelSize(Settings::water().mSmallFeatureCullingPixelSize); camera->setName("RefractionCamera"); camera->addCullCallback(new InheritViewPointCallback); camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); @@ -262,8 +275,8 @@ namespace MWRender camera->addChild(mClipCullNode); camera->setNodeMask(Mask_RenderToTexture); - if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 - SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + if (Settings::water().mRefractionScale != 1) // TODO: to be removed with issue #5709 + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); } void apply(osg::Camera* camera) override @@ -282,8 +295,7 @@ namespace MWRender void setWaterLevel(float waterLevel) { - const float refractionScale - = std::clamp(Settings::Manager::getFloat("refraction scale", "Water"), 0.f, 1.f); + const float refractionScale = Settings::water().mRefractionScale; mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); @@ -315,7 +327,7 @@ namespace MWRender { public: Reflection(uint32_t rttSize, bool isInterior) - : RTTNode(rttSize, rttSize, 0, false, 0, StereoAwareness::Aware) + : RTTNode(rttSize, rttSize, 0, false, 0, StereoAwareness::Aware, shouldAddMSAAIntermediateTarget()) { setInterior(isInterior); setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); @@ -325,8 +337,7 @@ namespace MWRender void setDefaults(osg::Camera* camera) override { camera->setReferenceFrame(osg::Camera::RELATIVE_RF); - camera->setSmallFeatureCullingPixelSize( - Settings::Manager::getInt("small feature culling pixel size", "Water")); + camera->setSmallFeatureCullingPixelSize(Settings::water().mSmallFeatureCullingPixelSize); camera->setName("ReflectionCamera"); camera->addCullCallback(new InheritViewPointCallback); @@ -341,7 +352,7 @@ namespace MWRender camera->addChild(mClipCullNode); camera->setNodeMask(Mask_RenderToTexture); - SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); } void apply(osg::Camera* camera) override @@ -381,7 +392,7 @@ namespace MWRender private: unsigned int calcNodeMask() { - int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); + int reflectionDetail = Settings::water().mReflectionDetail; reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5); unsigned int extraMask = 0; if (reflectionDetail >= 1) @@ -429,7 +440,7 @@ namespace MWRender Water::Water(osg::Group* parent, osg::Group* sceneRoot, Resource::ResourceSystem* resourceSystem, osgUtil::IncrementalCompileOperation* ico) - : mRainIntensityUpdater(nullptr) + : mRainSettingsUpdater(nullptr) , mParent(parent) , mSceneRoot(sceneRoot) , mResourceSystem(resourceSystem) @@ -522,9 +533,9 @@ namespace MWRender mWaterGeom->setStateSet(nullptr); mWaterGeom->setUpdateCallback(nullptr); - if (Settings::Manager::getBool("shader", "Water")) + if (Settings::water().mShader) { - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); + const unsigned int rttSize = Settings::water().mRttSize; mReflection = new Reflection(rttSize, mInterior); mReflection->setWaterLevel(mTop); @@ -533,7 +544,7 @@ namespace MWRender mReflection->addCullCallback(mCullCallback); mParent->addChild(mReflection); - if (Settings::Manager::getBool("refraction", "Water")) + if (Settings::water().mRefraction) { mRefraction = new Refraction(rttSize); mRefraction->setWaterLevel(mTop); @@ -557,16 +568,6 @@ namespace MWRender updateVisible(); } - osg::Node* Water::getReflectionNode() - { - return mReflection; - } - - osg::Node* Water::getRefractionNode() - { - return mRefraction; - } - osg::Vec3d Water::getPosition() const { return mWaterNode->getPosition(); @@ -578,7 +579,7 @@ namespace MWRender node->setStateSet(stateset); node->setUpdateCallback(nullptr); - mRainIntensityUpdater = nullptr; + mRainSettingsUpdater = nullptr; // Add animated textures std::vector> textures; @@ -588,8 +589,8 @@ namespace MWRender { std::ostringstream texname; texname << "textures/water/" << texture << std::setw(2) << std::setfill('0') << i << ".dds"; - osg::ref_ptr tex( - new osg::Texture2D(mResourceSystem->getImageManager()->getImage(texname.str()))); + const VFS::Path::Normalized path(texname.str()); + osg::ref_ptr tex(new osg::Texture2D(mResourceSystem->getImageManager()->getImage(path))); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mResourceSystem->getSceneManager()->applyFilterSettings(tex); @@ -689,32 +690,31 @@ namespace MWRender { // use a define map to conditionally compile the shader std::map defineMap; - defineMap["refraction_enabled"] = std::string(mRefraction ? "1" : "0"); - const auto rippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2); - defineMap["rain_ripple_detail"] = std::to_string(rippleDetail); - defineMap["ripple_map_world_scale"] = std::to_string(RipplesSurface::mWorldScaleFactor); - defineMap["ripple_map_size"] = std::to_string(RipplesSurface::mRTTSize) + ".0"; + defineMap["waterRefraction"] = std::string(mRefraction ? "1" : "0"); + const int rippleDetail = Settings::water().mRainRippleDetail; + defineMap["rainRippleDetail"] = std::to_string(rippleDetail); + defineMap["rippleMapWorldScale"] = std::to_string(RipplesSurface::sWorldScaleFactor); + defineMap["rippleMapSize"] = std::to_string(RipplesSurface::sRTTSize) + ".0"; + defineMap["sunlightScattering"] = Settings::water().mSunlightScattering ? "1" : "0"; + defineMap["wobblyShores"] = Settings::water().mWobblyShores ? "1" : "0"; Stereo::shaderStereoDefines(defineMap); Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr program = shaderMgr.getProgram("water", defineMap); + constexpr VFS::Path::NormalizedView waterImage("textures/omw/water_nm.png"); osg::ref_ptr normalMap( - new osg::Texture2D(mResourceSystem->getImageManager()->getImage("textures/omw/water_nm.png"))); - if (normalMap->getImage()) - normalMap->getImage()->flipVertical(); + new osg::Texture2D(mResourceSystem->getImageManager()->getImage(waterImage))); normalMap->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); normalMap->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - normalMap->setMaxAnisotropy(16); - normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); - normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + mResourceSystem->getSceneManager()->applyFilterSettings(normalMap); - mRainIntensityUpdater = new RainIntensityUpdater(); - node->setUpdateCallback(mRainIntensityUpdater); + mRainSettingsUpdater = new RainSettingsUpdater(); + node->setUpdateCallback(mRainSettingsUpdater); - mShaderWaterStateSetUpdater - = new ShaderWaterStateSetUpdater(this, mReflection, mRefraction, mRipples, std::move(program), normalMap); + mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater( + this, mReflection, mRefraction, mRipples, std::move(program), std::move(normalMap)); node->addCullCallback(mShaderWaterStateSetUpdater); } @@ -745,7 +745,7 @@ namespace MWRender } } - void Water::listAssetsToPreload(std::vector& textures) + void Water::listAssetsToPreload(std::vector& textures) { const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); std::string_view texture = Fallback::Map::getString("Water_SurfaceTexture"); @@ -753,7 +753,7 @@ namespace MWRender { std::ostringstream texname; texname << "textures/water/" << texture << std::setw(2) << std::setfill('0') << i << ".dds"; - textures.push_back(texname.str()); + textures.emplace_back(texname.str()); } } @@ -800,8 +800,14 @@ namespace MWRender void Water::setRainIntensity(float rainIntensity) { - if (mRainIntensityUpdater) - mRainIntensityUpdater->setRainIntensity(rainIntensity); + if (mRainSettingsUpdater) + mRainSettingsUpdater->setRainIntensity(rainIntensity); + } + + void Water::setRainRipplesEnabled(bool enableRipples) + { + if (mRainSettingsUpdater) + mRainSettingsUpdater->setRipplesEnabled(enableRipples); } void Water::update(float dt, bool paused) diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 0204cb4303..92299309f0 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -9,6 +9,7 @@ #include #include +#include namespace osg { @@ -46,13 +47,13 @@ namespace MWRender class Refraction; class Reflection; class RippleSimulation; - class RainIntensityUpdater; + class RainSettingsUpdater; class Ripples; /// Water rendering class Water { - osg::ref_ptr mRainIntensityUpdater; + osg::ref_ptr mRainSettingsUpdater; osg::ref_ptr mParent; osg::ref_ptr mSceneRoot; @@ -92,7 +93,7 @@ namespace MWRender void setCullCallback(osg::Callback* callback); - void listAssetsToPreload(std::vector& textures); + void listAssetsToPreload(std::vector& textures); void setEnabled(bool enabled); @@ -113,12 +114,10 @@ namespace MWRender void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); void setRainIntensity(const float rainIntensity); + void setRainRipplesEnabled(bool enableRipples); void update(float dt, bool paused); - osg::Node* getReflectionNode(); - osg::Node* getRefractionNode(); - osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index c19062168e..b9c8fd1d28 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -86,7 +86,7 @@ namespace MWRender MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; - std::string model = ammo->getClass().getModel(*ammo); + VFS::Path::Normalized model(ammo->getClass().getCorrectedModel(*ammo)); osg::ref_ptr arrow = getResourceSystem()->getSceneManager()->getInstance(model, parent); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index b6acbd246b..a91a585367 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -507,8 +507,9 @@ namespace MWScript runtime.pop(); MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false); - if (!target.isEmpty()) - MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target); + if (!target.isEmpty() && !target.getBase()->isDeleted() + && !target.getClass().getCreatureStats(target).isDead()) + MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target, nullptr); } }; diff --git a/apps/openmw/mwscript/animationextensions.cpp b/apps/openmw/mwscript/animationextensions.cpp index 32d7e46527..16c1f5a134 100644 --- a/apps/openmw/mwscript/animationextensions.cpp +++ b/apps/openmw/mwscript/animationextensions.cpp @@ -56,7 +56,7 @@ namespace MWScript } MWBase::Environment::get().getMechanicsManager()->playAnimationGroup( - ptr, group, mode, std::numeric_limits::max(), true); + ptr, group, mode, std::numeric_limits::max(), true); } }; @@ -91,7 +91,7 @@ namespace MWScript throw std::runtime_error("animation mode out of range"); } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops + 1, true); + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops, true); } }; diff --git a/apps/openmw/mwscript/cellextensions.cpp b/apps/openmw/mwscript/cellextensions.cpp index 3a8318a7a3..d913eb0915 100644 --- a/apps/openmw/mwscript/cellextensions.cpp +++ b/apps/openmw/mwscript/cellextensions.cpp @@ -11,6 +11,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -123,7 +125,7 @@ namespace MWScript MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr playerPtr = world->getPlayerPtr(); - osg::Vec2 posFromIndex + const osg::Vec2f posFromIndex = ESM::indexToPosition(ESM::ExteriorCellLocation(x, y, ESM::Cell::sDefaultWorldspaceId), true); pos.pos[0] = posFromIndex.x(); pos.pos[1] = posFromIndex.y(); diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index edee3963e7..9708a503ee 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -31,6 +31,7 @@ #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/levelledlist.hpp" +#include "interpretercontext.hpp" #include "ref.hpp" namespace @@ -94,6 +95,12 @@ namespace MWScript Interpreter::Type_Integer count = runtime[0].mInteger; runtime.pop(); + if (!MWBase::Environment::get().getESMStore()->find(item)) + { + runtime.getContext().report("Failed to add item '" + item.getRefIdString() + "': unknown ID"); + return; + } + if (count < 0) count = static_cast(count); @@ -102,7 +109,7 @@ namespace MWScript return; if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") - item = ESM::RefId::stringRefId("gold_001"); + item = MWWorld::ContainerStore::sGoldId; // Check if "item" can be placed in a container MWWorld::ManualRef manualRef(*MWBase::Environment::get().getESMStore(), item, 1); @@ -182,13 +189,19 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime, false); ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (ptr.isEmpty() || (ptr.getType() != ESM::Container::sRecordId && !ptr.getClass().isActor())) + { + runtime.push(0); + return; + } + if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") - item = ESM::RefId::stringRefId("gold_001"); + item = MWWorld::ContainerStore::sGoldId; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); @@ -210,6 +223,12 @@ namespace MWScript Interpreter::Type_Integer count = runtime[0].mInteger; runtime.pop(); + if (!MWBase::Environment::get().getESMStore()->find(item)) + { + runtime.getContext().report("Failed to remove item '" + item.getRefIdString() + "': unknown ID"); + return; + } + if (count < 0) count = static_cast(count); @@ -218,7 +237,7 @@ namespace MWScript return; if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") - item = ESM::RefId::stringRefId("gold_001"); + item = MWWorld::ContainerStore::sGoldId; // Explicit calls to non-unique actors affect the base record if (!R::implicit && ptr.getClass().isActor() @@ -447,7 +466,7 @@ namespace MWScript it != invStore.cend(); ++it) { if (it->getCellRef().getSoul() == name) - count += it->getRefData().getCount(); + count += it->getCellRef().getCount(); } runtime.push(count); } diff --git a/apps/openmw/mwscript/controlextensions.cpp b/apps/openmw/mwscript/controlextensions.cpp index a69f2cd571..b9e8f8965a 100644 --- a/apps/openmw/mwscript/controlextensions.cpp +++ b/apps/openmw/mwscript/controlextensions.cpp @@ -25,11 +25,11 @@ namespace MWScript { class OpSetControl : public Interpreter::Opcode0 { - std::string mControl; + std::string_view mControl; bool mEnable; public: - OpSetControl(const std::string& control, bool enable) + OpSetControl(std::string_view control, bool enable) : mControl(control) , mEnable(enable) { @@ -43,10 +43,10 @@ namespace MWScript class OpGetDisabled : public Interpreter::Opcode0 { - std::string mControl; + std::string_view mControl; public: - OpGetDisabled(const std::string& control) + OpGetDisabled(std::string_view control) : mControl(control) { } diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index 5a361e1bdc..6511fbdb01 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -16,6 +16,7 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "ref.hpp" @@ -89,6 +90,13 @@ namespace MWScript ESM::RefId topic = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!MWBase::Environment::get().getESMStore()->get().search(topic)) + { + runtime.getContext().report( + "Failed to add topic '" + topic.getRefIdString() + "': topic record not found"); + return; + } + MWBase::Environment::get().getDialogueManager()->addTopic(topic); } }; @@ -135,6 +143,15 @@ namespace MWScript return; } + bool greetWerewolves = false; + const ESM::RefId& script = ptr.getClass().getScript(ptr); + if (!script.empty()) + greetWerewolves = ptr.getRefData().getLocals().hasVar(script, "allowwerewolfforcegreeting"); + + const MWWorld::Ptr& player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getNpcStats(player).isWerewolf() && !greetWerewolves) + return; + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, ptr); } }; diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index c1590941e6..07855f18ef 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -192,7 +192,8 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - bool state = MWBase::Environment::get().getWindowManager()->toggleHud(); + bool state = MWBase::Environment::get().getWindowManager()->setHudVisibility( + !MWBase::Environment::get().getWindowManager()->isHudVisible()); runtime.getContext().report(state ? "GUI -> On" : "GUI -> Off"); if (!state) diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index db5dda5204..3bd4e82059 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -121,6 +121,12 @@ namespace MWScript return true; } + std::size_t Locals::getSize(const ESM::RefId& script) + { + ensure(script); + return mShorts.size() + mLongs.size() + mFloats.size(); + } + bool Locals::write(ESM::Locals& locals, const ESM::RefId& script) const { if (!mInitialised) diff --git a/apps/openmw/mwscript/locals.hpp b/apps/openmw/mwscript/locals.hpp index 1cc8fecb9b..76b582b78c 100644 --- a/apps/openmw/mwscript/locals.hpp +++ b/apps/openmw/mwscript/locals.hpp @@ -67,6 +67,8 @@ namespace MWScript return static_cast(getVarAsDouble(script, var)); } + std::size_t getSize(const ESM::RefId& script); + /// \note If locals have not been configured yet, no data is written. /// /// \return Locals written? diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 43da00afe3..72faf0afa9 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -41,11 +41,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -105,7 +107,7 @@ namespace std::string fileName; if (image) fileName = image->getFileName(); - mTextures.emplace_back(texture->getName(), fileName); + mTextures.emplace_back(SceneUtil::getTextureType(*stateset, *texture, i), fileName); } } @@ -184,6 +186,14 @@ namespace MWScript MWWorld::Ptr target = R()(runtime, false); ESM::RefId name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + + if (!MWBase::Environment::get().getESMStore()->get().search(name)) + { + runtime.getContext().report( + "Failed to start global script '" + name.getRefIdString() + "': script record not found"); + return; + } + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript(name, target); } }; @@ -206,6 +216,14 @@ namespace MWScript { const ESM::RefId& name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + + if (!MWBase::Environment::get().getESMStore()->get().search(name)) + { + runtime.getContext().report( + "Failed to stop global script '" + name.getRefIdString() + "': script record not found"); + return; + } + MWBase::Environment::get().getScriptManager()->getGlobalScripts().removeScript(name); } }; @@ -585,15 +603,15 @@ namespace MWScript key = ESM::MagicEffect::effectGmstIdToIndex(effect); const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - - const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); - - for (const auto& activeEffect : effects) + for (const auto& spell : stats.getActiveSpells()) { - if (activeEffect.first.mId == key && activeEffect.second.getModifier() > 0) + for (const auto& effect : spell.getEffects()) { - runtime.push(1); - return; + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId == key) + { + runtime.push(1); + return; + } } } runtime.push(0); @@ -614,7 +632,7 @@ namespace MWScript ESM::RefId gem = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) + if (!ptr.getClass().isActor()) return; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); @@ -647,10 +665,10 @@ namespace MWScript for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) + if (!ptr.getClass().isActor()) return; - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (it->getCellRef().getSoul() == soul) @@ -687,46 +705,48 @@ namespace MWScript if (!ptr.getClass().isActor()) return; + MWWorld::InventoryStore* invStorePtr = nullptr; if (ptr.getClass().hasInventoryStore(ptr)) { + invStorePtr = &ptr.getClass().getInventoryStore(ptr); // Prefer dropping unequipped items first; re-stack if possible by unequipping items before dropping // them. - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - int numNotEquipped = store.count(item); + int numNotEquipped = invStorePtr->count(item); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { - MWWorld::ConstContainerStoreIterator it = store.getSlot(slot); - if (it != store.end() && it->getCellRef().getRefId() == item) + MWWorld::ConstContainerStoreIterator it = invStorePtr->getSlot(slot); + if (it != invStorePtr->end() && it->getCellRef().getRefId() == item) { - numNotEquipped -= it->getRefData().getCount(); + numNotEquipped -= it->getCellRef().getCount(); } } for (int slot = 0; slot < MWWorld::InventoryStore::Slots && amount > numNotEquipped; ++slot) { - MWWorld::ContainerStoreIterator it = store.getSlot(slot); - if (it != store.end() && it->getCellRef().getRefId() == item) + MWWorld::ContainerStoreIterator it = invStorePtr->getSlot(slot); + if (it != invStorePtr->end() && it->getCellRef().getRefId() == item) { - int numToRemove = std::min(amount - numNotEquipped, it->getRefData().getCount()); - store.unequipItemQuantity(*it, numToRemove); + int numToRemove = std::min(amount - numNotEquipped, it->getCellRef().getCount()); + invStorePtr->unequipItemQuantity(*it, numToRemove); numNotEquipped += numToRemove; } } + } - for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) + { + if (iter->getCellRef().getRefId() == item && (!invStorePtr || !invStorePtr->isEquipped(*iter))) { - if (iter->getCellRef().getRefId() == item && !store.isEquipped(*iter)) - { - int removed = store.remove(*iter, amount); - MWWorld::Ptr dropped - = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); - dropped.getCellRef().setOwner(ESM::RefId()); + int removed = store.remove(*iter, amount); + MWWorld::Ptr dropped + = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); + dropped.getCellRef().setOwner(ESM::RefId()); - amount -= removed; + amount -= removed; - if (amount <= 0) - break; - } + if (amount <= 0) + break; } } @@ -763,10 +783,10 @@ namespace MWScript ESM::RefId soul = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) + if (!ptr.getClass().isActor()) return; - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) { @@ -1329,8 +1349,10 @@ namespace MWScript { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); - MWBase::Environment::get().getWorld()->confiscateStolenItems(player); - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + world->confiscateStolenItems(player); + world->getPlayer().recordCrimeId(); + world->getPlayer().setDrawState(MWMechanics::DrawState::Nothing); } }; @@ -1399,7 +1421,7 @@ namespace MWScript if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; - if (!ptr.getRefData().getCount()) + if (!ptr.getCellRef().getCount()) msg << "[Deleted]" << std::endl; msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; @@ -1415,9 +1437,9 @@ namespace MWScript osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); msg << "Coordinates: " << pos.x() << " " << pos.y() << " " << pos.z() << std::endl; auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - std::string model - = ::Misc::ResourceHelpers::correctActorModelPath(ptr.getClass().getModel(ptr), vfs); - msg << "Model: " << model << std::endl; + const VFS::Path::Normalized model + = ::Misc::ResourceHelpers::correctActorModelPath(ptr.getClass().getCorrectedModel(ptr), vfs); + msg << "Model: " << model.value() << std::endl; if (!model.empty()) { const std::string archive = vfs->getArchive(model); @@ -1454,7 +1476,7 @@ namespace MWScript if (lastTextureSrc.empty() || textureSrc != lastTextureSrc) { - lastTextureSrc = textureSrc; + lastTextureSrc = std::move(textureSrc); if (lastTextureSrc.empty()) lastTextureSrc = "[No Source]"; @@ -1692,7 +1714,7 @@ namespace MWScript for (const T& record : store.get()) { MWWorld::ManualRef ref(store, record.mId); - std::string model = ref.getPtr().getClass().getModel(ref.getPtr()); + VFS::Path::Normalized model(ref.getPtr().getClass().getCorrectedModel(ref.getPtr())); if (!model.empty()) { sceneManager->getTemplate(model); diff --git a/apps/openmw/mwscript/scriptmanagerimp.cpp b/apps/openmw/mwscript/scriptmanagerimp.cpp index 14194de61d..e8d57b4c74 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.cpp +++ b/apps/openmw/mwscript/scriptmanagerimp.cpp @@ -24,20 +24,16 @@ namespace MWScript { - ScriptManager::ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode, - const std::vector& scriptBlacklist) + ScriptManager::ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode) : mErrorHandler() , mStore(store) , mCompilerContext(compilerContext) , mParser(mErrorHandler, mCompilerContext) - , mOpcodesInstalled(false) , mGlobalScripts(store) { + installOpcodes(mInterpreter); + mErrorHandler.setWarningsMode(warningsMode); - - mScriptBlacklist.resize(scriptBlacklist.size()); - - std::sort(mScriptBlacklist.begin(), mScriptBlacklist.end()); } bool ScriptManager::compile(const ESM::RefId& name) @@ -110,14 +106,9 @@ namespace MWScript const auto& target = interpreterContext.getTarget(); if (!iter->second.mProgram.mInstructions.empty() && iter->second.mInactive.find(target) == iter->second.mInactive.end()) + { try { - if (!mOpcodesInstalled) - { - installOpcodes(mInterpreter); - mOpcodesInstalled = true; - } - mInterpreter.run(iter->second.mProgram, interpreterContext); return true; } @@ -131,6 +122,7 @@ namespace MWScript iter->second.mInactive.insert(target); // don't execute again. } + } return false; } @@ -151,13 +143,10 @@ namespace MWScript for (auto& script : mStore.get()) { - if (!std::binary_search(mScriptBlacklist.begin(), mScriptBlacklist.end(), script.mId)) - { - ++count; + ++count; - if (compile(script.mId)) - ++success; - } + if (compile(script.mId)) + ++success; } return std::make_pair(count, success); diff --git a/apps/openmw/mwscript/scriptmanagerimp.hpp b/apps/openmw/mwscript/scriptmanagerimp.hpp index de1ce286a6..b17057ae28 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.hpp +++ b/apps/openmw/mwscript/scriptmanagerimp.hpp @@ -42,7 +42,6 @@ namespace MWScript Compiler::Context& mCompilerContext; Compiler::FileParser mParser; Interpreter::Interpreter mInterpreter; - bool mOpcodesInstalled; struct CompiledScript { @@ -60,11 +59,9 @@ namespace MWScript std::unordered_map mScripts; GlobalScripts mGlobalScripts; std::unordered_map mOtherLocals; - std::vector mScriptBlacklist; public: - ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode, - const std::vector& scriptBlacklist); + ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode); void clear() override; diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 39944970bf..d2b41fb87a 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -102,11 +102,11 @@ namespace MWScript std::string_view region{ runtime.getStringLiteral(runtime[0].mInteger) }; runtime.pop(); - std::vector chances; + std::vector chances; chances.reserve(10); while (arg0 > 0) { - chances.push_back(std::clamp(runtime[0].mInteger, 0, 127)); + chances.push_back(std::clamp(runtime[0].mInteger, 0, 100)); runtime.pop(); arg0--; } diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index f1ac2a7a08..c248c30520 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -33,7 +33,7 @@ namespace MWScript MWScript::InterpreterContext& context = static_cast(runtime.getContext()); - std::string file{ runtime.getStringLiteral(runtime[0].mInteger) }; + VFS::Path::Normalized file{ runtime.getStringLiteral(runtime[0].mInteger) }; runtime.pop(); std::string_view text = runtime.getStringLiteral(runtime[0].mInteger); @@ -63,10 +63,11 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - std::string sound{ runtime.getStringLiteral(runtime[0].mInteger) }; + const VFS::Path::Normalized music(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - MWBase::Environment::get().getSoundManager()->streamMusic(sound); + MWBase::Environment::get().getSoundManager()->streamMusic( + Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::MWScript); } }; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 0363f21fa6..96a9c4f507 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -85,7 +85,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).getLevel(); + Interpreter::Type_Integer value = -1; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getLevel(); runtime.push(value); } @@ -102,7 +104,8 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats(ptr).setLevel(value); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).setLevel(value); } }; @@ -121,7 +124,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); + Interpreter::Type_Float value = 0.f; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); runtime.push(value); } @@ -145,6 +150,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); attribute.setBase(value, true); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); @@ -169,6 +177,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); modStat(attribute, value); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); @@ -189,14 +200,14 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value; + Interpreter::Type_Float value = 0.f; if (mIndex == 0 && ptr.getClass().hasItemHealth(ptr)) { // health is a special case value = static_cast(ptr.getClass().getItemMaxHealth(ptr)); } - else + else if (ptr.getClass().isActor()) { value = ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).getCurrent(); // GetMagicka shouldn't return negative values @@ -225,6 +236,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::DynamicStat stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex)); stat.setBase(value); @@ -254,6 +268,9 @@ namespace MWScript Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + // workaround broken endgame scripts that kill dagoth ur if (!R::implicit && ptr.getCellRef().getRefId() == "dagoth_ur_1") { @@ -301,6 +318,9 @@ namespace MWScript Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); @@ -336,9 +356,16 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + if (!ptr.getClass().isActor()) + { + runtime.push(0.f); + return; + } + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getDynamic(mIndex).getRatio()); + runtime.push(stats.getDynamic(mIndex).getRatio(false)); } }; @@ -357,6 +384,12 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0.f); + return; + } + Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mId); runtime.push(value); @@ -381,6 +414,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isNpc()) + return; + MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats(ptr); stats.getSkill(mId).setBase(value, true); @@ -405,6 +441,9 @@ namespace MWScript Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); + if (!ptr.getClass().isNpc()) + return; + MWMechanics::SkillValue& skill = ptr.getClass().getNpcStats(ptr).getSkill(mId); modStat(skill, value); } @@ -445,10 +484,12 @@ namespace MWScript { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - - player.getClass().getNpcStats(player).setBounty( - static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); + int bounty = std::max( + 0, static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); + player.getClass().getNpcStats(player).setBounty(bounty); runtime.pop(); + if (bounty == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; @@ -463,6 +504,9 @@ namespace MWScript ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!ptr.getClass().isActor()) + return; + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(id); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); @@ -489,6 +533,9 @@ namespace MWScript ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); + if (!ptr.getClass().isActor()) + return; + MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); creatureStats.getSpells().remove(id); @@ -512,7 +559,8 @@ namespace MWScript ESM::RefId spellid = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid); } }; @@ -527,7 +575,8 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; @@ -843,7 +892,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + else + runtime.push(0); } }; @@ -855,7 +907,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + else + runtime.push(0); } }; @@ -870,9 +925,16 @@ namespace MWScript ESM::RefId race = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const ESM::RefId& npcRace = ptr.get()->mBase->mRace; + if (ptr.getClass().isNpc()) + { + const ESM::RefId& npcRace = ptr.get()->mBase->mRace; - runtime.push(race == npcRace); + runtime.push(race == npcRace); + } + else + { + runtime.push(0); + } } }; @@ -938,7 +1000,7 @@ namespace MWScript MWWorld::Ptr player = MWMechanics::getPlayer(); if (!factionID.empty()) { - player.getClass().getNpcStats(player).expell(factionID); + player.getClass().getNpcStats(player).expell(factionID, true); } } }; @@ -1041,10 +1103,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).hasDied(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasDied(); - if (value) - ptr.getClass().getCreatureStats(ptr).clearHasDied(); + if (value) + stats.clearHasDied(); + } runtime.push(value); } @@ -1058,10 +1125,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).hasBeenMurdered(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasBeenMurdered(); - if (value) - ptr.getClass().getCreatureStats(ptr).clearHasBeenMurdered(); + if (value) + stats.clearHasBeenMurdered(); + } runtime.push(value); } @@ -1075,7 +1147,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); runtime.push(value); } @@ -1088,7 +1162,10 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); + if (ptr.getClass().isNpc()) + runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); + else + runtime.push(0); } }; @@ -1099,7 +1176,8 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); + if (ptr.getClass().isNpc()) + MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); } }; @@ -1110,7 +1188,8 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); + if (ptr.getClass().isNpc()) + MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); } }; @@ -1122,6 +1201,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + return; + if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); @@ -1190,6 +1272,12 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0); + return; + } + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) @@ -1224,6 +1312,13 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + int arg = runtime[0].mInteger; + runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) @@ -1237,8 +1332,6 @@ namespace MWScript if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); - int arg = runtime[0].mInteger; - runtime.pop(); effects.modifyBase(mPositiveEffect, (arg - static_cast(currentValue))); } }; @@ -1259,10 +1352,14 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); int arg = runtime[0].mInteger; runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); stats.getMagicEffects().modifyBase(mPositiveEffect, arg); } }; diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 614fbbe26f..bf09269f1e 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -4,6 +4,8 @@ #include +#include + #include #include @@ -166,7 +168,7 @@ namespace MWScript float az = ptr.getRefData().getPosition().rot[2]; // XYZ axis use the inverse (XYZ) rotation order like vanilla SetAngle. - // UWV axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. + // UVW axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. if (axis == "x") MWBase::Environment::get().getWorld()->rotateObject( ptr, osg::Vec3f(angle, ay, az), MWBase::RotationFlag_inverseOrder); @@ -179,10 +181,10 @@ namespace MWScript else if (axis == "u") MWBase::Environment::get().getWorld()->rotateObject( ptr, osg::Vec3f(angle, ay, az), MWBase::RotationFlag_none); - else if (axis == "w") + else if (axis == "v") MWBase::Environment::get().getWorld()->rotateObject( ptr, osg::Vec3f(ax, angle, az), MWBase::RotationFlag_none); - else if (axis == "v") + else if (axis == "w") MWBase::Environment::get().getWorld()->rotateObject( ptr, osg::Vec3f(ax, ay, angle), MWBase::RotationFlag_none); } diff --git a/apps/openmw/mwsound/constants.hpp b/apps/openmw/mwsound/constants.hpp new file mode 100644 index 0000000000..217dd1935e --- /dev/null +++ b/apps/openmw/mwsound/constants.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H +#define OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H + +#include + +namespace MWSound +{ + constexpr VFS::Path::NormalizedView battlePlaylist("battle"); + constexpr VFS::Path::NormalizedView explorePlaylist("explore"); + constexpr VFS::Path::NormalizedView titleMusic("music/special/morrowind title.mp3"); + constexpr VFS::Path::NormalizedView triumphMusic("music/special/mw_triumph.mp3"); + constexpr VFS::Path::NormalizedView deathMusic("music/special/mw_death.mp3"); +} + +#endif diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index bd63d3de40..54fd126c41 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -1,15 +1,48 @@ #include "ffmpeg_decoder.hpp" -#include - #include +#include #include +#include #include #include +#include +#include + +#if OPENMW_FFMPEG_5_OR_GREATER +#include +#endif + namespace MWSound { + void AVIOContextDeleter::operator()(AVIOContext* ptr) const + { + if (ptr->buffer != nullptr) + av_freep(&ptr->buffer); + +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100) + avio_context_free(&ptr); +#else + av_free(ptr); +#endif + } + + void AVFormatContextDeleter::operator()(AVFormatContext* ptr) const + { + avformat_close_input(&ptr); + } + + void AVCodecContextDeleter::operator()(AVCodecContext* ptr) const + { + avcodec_free_context(&ptr); + } + + void AVFrameDeleter::operator()(AVFrame* ptr) const + { + av_frame_free(&ptr); + } int FFmpeg_Decoder::readPacket(void* user_data, uint8_t* buf, int buf_size) { @@ -21,7 +54,9 @@ namespace MWSound std::streamsize count = stream.gcount(); if (count == 0) return AVERROR_EOF; - return count; + if (count > std::numeric_limits::max()) + return AVERROR_BUG; + return static_cast(count); } catch (std::exception&) { @@ -29,7 +64,11 @@ namespace MWSound } } +#if OPENMW_FFMPEG_CONST_WRITEPACKET + int FFmpeg_Decoder::writePacket(void*, const uint8_t*, int) +#else int FFmpeg_Decoder::writePacket(void*, uint8_t*, int) +#endif { Log(Debug::Error) << "can't write to read-only stream"; return -1; @@ -72,8 +111,8 @@ namespace MWSound if (!mStream) return false; - int stream_idx = mStream - mFormatCtx->streams; - while (av_read_frame(mFormatCtx, &mPacket) >= 0) + std::ptrdiff_t stream_idx = mStream - mFormatCtx->streams; + while (av_read_frame(mFormatCtx.get(), &mPacket) >= 0) { /* Check if the packet belongs to this stream */ if (stream_idx == mPacket.stream_index) @@ -100,12 +139,12 @@ namespace MWSound do { /* Decode some data, and check for errors */ - int ret = avcodec_receive_frame(mCodecCtx, mFrame); + int ret = avcodec_receive_frame(mCodecCtx.get(), mFrame.get()); if (ret == AVERROR(EAGAIN)) { if (mPacket.size == 0 && !getNextPacket()) return false; - ret = avcodec_send_packet(mCodecCtx, &mPacket); + ret = avcodec_send_packet(mCodecCtx.get(), &mPacket); av_packet_unref(&mPacket); if (ret == 0) continue; @@ -124,7 +163,11 @@ namespace MWSound if (!mDataBuf || mDataBufLen < mFrame->nb_samples) { av_freep(&mDataBuf); +#if OPENMW_FFMPEG_5_OR_GREATER + if (av_samples_alloc(&mDataBuf, nullptr, mOutputChannelLayout.nb_channels, +#else if (av_samples_alloc(&mDataBuf, nullptr, av_get_channel_layout_nb_channels(mOutputChannelLayout), +#endif mFrame->nb_samples, mOutputSampleFormat, 0) < 0) return false; @@ -161,7 +204,11 @@ namespace MWSound if (!getAVAudioData()) break; mFramePos = 0; +#if OPENMW_FFMPEG_5_OR_GREATER + mFrameSize = mFrame->nb_samples * mOutputChannelLayout.nb_channels +#else mFrameSize = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif * av_get_bytes_per_sample(mOutputSampleFormat); } @@ -180,142 +227,108 @@ namespace MWSound return dec; } - void FFmpeg_Decoder::open(const std::string& fname) + void FFmpeg_Decoder::open(VFS::Path::NormalizedView fname) { close(); mDataStream = mResourceMgr->get(fname); - if ((mFormatCtx = avformat_alloc_context()) == nullptr) + AVIOContextPtr ioCtx(avio_alloc_context(nullptr, 0, 0, this, readPacket, writePacket, seek)); + if (ioCtx == nullptr) + throw std::runtime_error("Failed to allocate AVIO context"); + + AVFormatContext* formatCtx = avformat_alloc_context(); + if (formatCtx == nullptr) throw std::runtime_error("Failed to allocate context"); - try + formatCtx->pb = ioCtx.get(); + + // avformat_open_input frees user supplied AVFormatContext on failure + if (avformat_open_input(&formatCtx, fname.value().data(), nullptr, nullptr) != 0) + throw std::runtime_error("Failed to open input"); + + AVFormatContextPtr formatCtxPtr(std::exchange(formatCtx, nullptr)); + + if (avformat_find_stream_info(formatCtxPtr.get(), nullptr) < 0) + throw std::runtime_error("Failed to find stream info"); + + AVStream** stream = nullptr; + for (size_t j = 0; j < formatCtxPtr->nb_streams; j++) { - mFormatCtx->pb = avio_alloc_context(nullptr, 0, 0, this, readPacket, writePacket, seek); - if (!mFormatCtx->pb || avformat_open_input(&mFormatCtx, fname.c_str(), nullptr, nullptr) != 0) + if (formatCtxPtr->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { - // "Note that a user-supplied AVFormatContext will be freed on failure". - if (mFormatCtx) - { - if (mFormatCtx->pb != nullptr) - { - if (mFormatCtx->pb->buffer != nullptr) - { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = nullptr; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = nullptr; - } - avformat_free_context(mFormatCtx); - } - mFormatCtx = nullptr; - throw std::runtime_error("Failed to allocate input stream"); + stream = &formatCtxPtr->streams[j]; + break; } + } - if (avformat_find_stream_info(mFormatCtx, nullptr) < 0) - throw std::runtime_error("Failed to find stream info in " + fname); + if (stream == nullptr) + throw std::runtime_error("No audio streams"); - for (size_t j = 0; j < mFormatCtx->nb_streams; j++) - { - if (mFormatCtx->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - { - mStream = &mFormatCtx->streams[j]; - break; - } - } - if (!mStream) - throw std::runtime_error("No audio streams in " + fname); + const AVCodec* codec = avcodec_find_decoder((*stream)->codecpar->codec_id); + if (codec == nullptr) + throw std::runtime_error("No codec found for id " + std::to_string((*stream)->codecpar->codec_id)); - const AVCodec* codec = avcodec_find_decoder((*mStream)->codecpar->codec_id); - if (!codec) - { - std::string ss = "No codec found for id " + std::to_string((*mStream)->codecpar->codec_id); - throw std::runtime_error(ss); - } + AVCodecContext* codecCtx = avcodec_alloc_context3(codec); + if (codecCtx == nullptr) + throw std::runtime_error("Failed to allocate codec context"); - AVCodecContext* avctx = avcodec_alloc_context3(codec); - avcodec_parameters_to_context(avctx, (*mStream)->codecpar); + avcodec_parameters_to_context(codecCtx, (*stream)->codecpar); // This is not needed anymore above FFMpeg version 4.0 #if LIBAVCODEC_VERSION_INT < 3805796 - av_codec_set_pkt_timebase(avctx, (*mStream)->time_base); + av_codec_set_pkt_timebase(avctx, (*stream)->time_base); #endif - mCodecCtx = avctx; + AVCodecContextPtr codecCtxPtr(std::exchange(codecCtx, nullptr)); - if (avcodec_open2(mCodecCtx, codec, nullptr) < 0) - throw std::runtime_error(std::string("Failed to open audio codec ") + codec->long_name); + if (avcodec_open2(codecCtxPtr.get(), codec, nullptr) < 0) + throw std::runtime_error(std::string("Failed to open audio codec ") + codec->long_name); - mFrame = av_frame_alloc(); + AVFramePtr frame(av_frame_alloc()); + if (frame == nullptr) + throw std::runtime_error("Failed to allocate frame"); - if (mCodecCtx->sample_fmt == AV_SAMPLE_FMT_U8P) - mOutputSampleFormat = AV_SAMPLE_FMT_U8; - // FIXME: Check for AL_EXT_FLOAT32 support - // else if (mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLT || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLTP) - // mOutputSampleFormat = AV_SAMPLE_FMT_S16; - else - mOutputSampleFormat = AV_SAMPLE_FMT_S16; + if (codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_U8P) + mOutputSampleFormat = AV_SAMPLE_FMT_U8; + // FIXME: Check for AL_EXT_FLOAT32 support + // else if (codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_FLT || codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_FLTP) + // mOutputSampleFormat = AV_SAMPLE_FMT_S16; + else + mOutputSampleFormat = AV_SAMPLE_FMT_S16; - mOutputChannelLayout = (*mStream)->codecpar->channel_layout; - if (mOutputChannelLayout == 0) - mOutputChannelLayout = av_get_default_channel_layout(mCodecCtx->channels); +#if OPENMW_FFMPEG_5_OR_GREATER + mOutputChannelLayout = (*stream)->codecpar->ch_layout; // sefault + if (mOutputChannelLayout.u.mask == 0) + av_channel_layout_default(&mOutputChannelLayout, codecCtxPtr->ch_layout.nb_channels); - mCodecCtx->channel_layout = mOutputChannelLayout; - } - catch (...) - { - if (mStream) - avcodec_free_context(&mCodecCtx); - mStream = nullptr; + codecCtxPtr->ch_layout = mOutputChannelLayout; +#else + mOutputChannelLayout = (*stream)->codecpar->channel_layout; + if (mOutputChannelLayout == 0) + mOutputChannelLayout = av_get_default_channel_layout(codecCtxPtr->channels); - if (mFormatCtx != nullptr) - { - if (mFormatCtx->pb->buffer != nullptr) - { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = nullptr; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = nullptr; + codecCtxPtr->channel_layout = mOutputChannelLayout; +#endif - avformat_close_input(&mFormatCtx); - } - } + mIoCtx = std::move(ioCtx); + mFrame = std::move(frame); + mFormatCtx = std::move(formatCtxPtr); + mCodecCtx = std::move(codecCtxPtr); + mStream = stream; } void FFmpeg_Decoder::close() { - if (mStream) - avcodec_free_context(&mCodecCtx); mStream = nullptr; + mCodecCtx.reset(); av_packet_unref(&mPacket); av_freep(&mDataBuf); - av_frame_free(&mFrame); + mFrame.reset(); swr_free(&mSwr); - if (mFormatCtx) - { - if (mFormatCtx->pb != nullptr) - { - // mFormatCtx->pb->buffer must be freed by hand, - // if not, valgrind will show memleak, see: - // - // https://trac.ffmpeg.org/ticket/1357 - // - if (mFormatCtx->pb->buffer != nullptr) - { - av_freep(&mFormatCtx->pb->buffer); - } -#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100) - avio_context_free(&mFormatCtx->pb); -#else - av_freep(&mFormatCtx->pb); -#endif - } - avformat_close_input(&mFormatCtx); - } - + mFormatCtx.reset(); + mIoCtx.reset(); mDataStream.reset(); } @@ -346,41 +359,88 @@ namespace MWSound *type = SampleType_Int16; } - if (mOutputChannelLayout == AV_CH_LAYOUT_MONO) - *chans = ChannelConfig_Mono; - else if (mOutputChannelLayout == AV_CH_LAYOUT_STEREO) - *chans = ChannelConfig_Stereo; - else if (mOutputChannelLayout == AV_CH_LAYOUT_QUAD) - *chans = ChannelConfig_Quad; - else if (mOutputChannelLayout == AV_CH_LAYOUT_5POINT1) - *chans = ChannelConfig_5point1; - else if (mOutputChannelLayout == AV_CH_LAYOUT_7POINT1) - *chans = ChannelConfig_7point1; - else +#if OPENMW_FFMPEG_5_OR_GREATER + switch (mOutputChannelLayout.u.mask) +#else + switch (mOutputChannelLayout) +#endif { - char str[1024]; - av_get_channel_layout_string(str, sizeof(str), mCodecCtx->channels, mCodecCtx->channel_layout); - Log(Debug::Error) << "Unsupported channel layout: " << str; - - if (mCodecCtx->channels == 1) - { - mOutputChannelLayout = AV_CH_LAYOUT_MONO; + case AV_CH_LAYOUT_MONO: *chans = ChannelConfig_Mono; - } - else - { - mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + break; + case AV_CH_LAYOUT_STEREO: *chans = ChannelConfig_Stereo; - } + break; + case AV_CH_LAYOUT_QUAD: + *chans = ChannelConfig_Quad; + break; + case AV_CH_LAYOUT_5POINT1: + *chans = ChannelConfig_5point1; + break; + case AV_CH_LAYOUT_7POINT1: + *chans = ChannelConfig_7point1; + break; + default: + char str[1024]; +#if OPENMW_FFMPEG_5_OR_GREATER + av_channel_layout_describe(&mCodecCtx->ch_layout, str, sizeof(str)); + Log(Debug::Error) << "Unsupported channel layout: " << str; + + if (mCodecCtx->ch_layout.nb_channels == 1) + { + mOutputChannelLayout = AV_CHANNEL_LAYOUT_MONO; + *chans = ChannelConfig_Mono; + } + else + { + mOutputChannelLayout = AV_CHANNEL_LAYOUT_STEREO; + *chans = ChannelConfig_Stereo; + } +#else + av_get_channel_layout_string(str, sizeof(str), mCodecCtx->channels, mCodecCtx->channel_layout); + Log(Debug::Error) << "Unsupported channel layout: " << str; + + if (mCodecCtx->channels == 1) + { + mOutputChannelLayout = AV_CH_LAYOUT_MONO; + *chans = ChannelConfig_Mono; + } + else + { + mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + *chans = ChannelConfig_Stereo; + } +#endif + break; } *samplerate = mCodecCtx->sample_rate; +#if OPENMW_FFMPEG_5_OR_GREATER + AVChannelLayout ch_layout = mCodecCtx->ch_layout; + if (ch_layout.u.mask == 0) + av_channel_layout_default(&ch_layout, mCodecCtx->ch_layout.nb_channels); + + if (mOutputSampleFormat != mCodecCtx->sample_fmt || mOutputChannelLayout.u.mask != ch_layout.u.mask) +#else int64_t ch_layout = mCodecCtx->channel_layout; if (ch_layout == 0) ch_layout = av_get_default_channel_layout(mCodecCtx->channels); if (mOutputSampleFormat != mCodecCtx->sample_fmt || mOutputChannelLayout != ch_layout) +#endif + { +#if OPENMW_FFMPEG_5_OR_GREATER + swr_alloc_set_opts2(&mSwr, // SwrContext + &mOutputChannelLayout, // output ch layout + mOutputSampleFormat, // output sample format + mCodecCtx->sample_rate, // output sample rate + &ch_layout, // input ch layout + mCodecCtx->sample_fmt, // input sample format + mCodecCtx->sample_rate, // input sample rate + 0, // logging level offset + nullptr); // log context +#else mSwr = swr_alloc_set_opts(mSwr, // SwrContext mOutputChannelLayout, // output ch layout mOutputSampleFormat, // output sample format @@ -390,6 +450,7 @@ namespace MWSound mCodecCtx->sample_rate, // input sample rate 0, // logging level offset nullptr); // log context +#endif if (!mSwr) throw std::runtime_error("Couldn't allocate SwrContext"); int init = swr_init(mSwr); @@ -418,7 +479,11 @@ namespace MWSound while (getAVAudioData()) { +#if OPENMW_FFMPEG_5_OR_GREATER + size_t got = mFrame->nb_samples * mOutputChannelLayout.nb_channels +#else size_t got = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif * av_get_bytes_per_sample(mOutputSampleFormat); const char* inbuf = reinterpret_cast(mFrameData[0]); output.insert(output.end(), inbuf, inbuf + got); @@ -427,23 +492,28 @@ namespace MWSound size_t FFmpeg_Decoder::getSampleOffset() { - int delay = (mFrameSize - mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) +#if OPENMW_FFMPEG_5_OR_GREATER + std::size_t delay = (mFrameSize - mFramePos) / mOutputChannelLayout.nb_channels +#else + std::size_t delay = (mFrameSize - mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif / av_get_bytes_per_sample(mOutputSampleFormat); - return (int)(mNextPts * mCodecCtx->sample_rate) - delay; + return static_cast(mNextPts * mCodecCtx->sample_rate) - delay; } FFmpeg_Decoder::FFmpeg_Decoder(const VFS::Manager* vfs) : Sound_Decoder(vfs) - , mFormatCtx(nullptr) - , mCodecCtx(nullptr) , mStream(nullptr) - , mFrame(nullptr) , mFrameSize(0) , mFramePos(0) , mNextPts(0.0) , mSwr(nullptr) , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) +#if OPENMW_FFMPEG_5_OR_GREATER + , mOutputChannelLayout({}) +#else , mOutputChannelLayout(0) +#endif , mDataBuf(nullptr) , mFrameData(nullptr) , mDataBufLen(0) @@ -468,5 +538,4 @@ namespace MWSound { close(); } - } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpeg_decoder.hpp index 88dd3316f5..e67b8efbf3 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.hpp @@ -3,6 +3,9 @@ #include +#include +#include + #if defined(_MSC_VER) #pragma warning(push) #pragma warning(disable : 4244) @@ -32,23 +35,56 @@ extern "C" namespace MWSound { + struct AVIOContextDeleter + { + void operator()(AVIOContext* ptr) const; + }; + + using AVIOContextPtr = std::unique_ptr; + + struct AVFormatContextDeleter + { + void operator()(AVFormatContext* ptr) const; + }; + + using AVFormatContextPtr = std::unique_ptr; + + struct AVCodecContextDeleter + { + void operator()(AVCodecContext* ptr) const; + }; + + using AVCodecContextPtr = std::unique_ptr; + + struct AVFrameDeleter + { + void operator()(AVFrame* ptr) const; + }; + + using AVFramePtr = std::unique_ptr; + class FFmpeg_Decoder final : public Sound_Decoder { - AVFormatContext* mFormatCtx; - AVCodecContext* mCodecCtx; + AVIOContextPtr mIoCtx; + AVFormatContextPtr mFormatCtx; + AVCodecContextPtr mCodecCtx; AVStream** mStream; AVPacket mPacket; - AVFrame* mFrame; + AVFramePtr mFrame; - int mFrameSize; - int mFramePos; + std::size_t mFrameSize; + std::size_t mFramePos; double mNextPts; SwrContext* mSwr; enum AVSampleFormat mOutputSampleFormat; +#if OPENMW_FFMPEG_5_OR_GREATER + AVChannelLayout mOutputChannelLayout; +#else int64_t mOutputChannelLayout; +#endif uint8_t* mDataBuf; uint8_t** mFrameData; int mDataBufLen; @@ -58,13 +94,17 @@ namespace MWSound Files::IStreamPtr mDataStream; static int readPacket(void* user_data, uint8_t* buf, int buf_size); +#if OPENMW_FFMPEG_CONST_WRITEPACKET + static int writePacket(void* user_data, const uint8_t* buf, int buf_size); +#else static int writePacket(void* user_data, uint8_t* buf, int buf_size); +#endif static int64_t seek(void* user_data, int64_t offset, int whence); bool getAVAudioData(); size_t readAVAudioData(void* data, size_t length); - void open(const std::string& fname) override; + void open(VFS::Path::NormalizedView fname) override; void close() override; std::string getName() override; diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp index b1c1a3f2af..2a6ac5ac8e 100644 --- a/apps/openmw/mwsound/loudness.cpp +++ b/apps/openmw/mwsound/loudness.cpp @@ -15,11 +15,11 @@ namespace MWSound return; int samplesPerSegment = static_cast(mSampleRate / mSamplesPerSec); - int numSamples = bytesToFrames(mQueue.size(), mChannelConfig, mSampleType); - int advance = framesToBytes(1, mChannelConfig, mSampleType); + std::size_t numSamples = bytesToFrames(mQueue.size(), mChannelConfig, mSampleType); + std::size_t advance = framesToBytes(1, mChannelConfig, mSampleType); - int segment = 0; - int sample = 0; + std::size_t segment = 0; + std::size_t sample = 0; while (segment < numSamples / samplesPerSegment) { float sum = 0; @@ -61,7 +61,7 @@ namespace MWSound if (mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; - size_t index = std::clamp(sec * mSamplesPerSec, 0, mSamples.size() - 1); + size_t index = std::min(static_cast(sec * mSamplesPerSec), mSamples.size() - 1); return mSamples[index]; } diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp index 1bb5275c45..962086701a 100644 --- a/apps/openmw/mwsound/movieaudiofactory.cpp +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -1,6 +1,7 @@ #include "movieaudiofactory.hpp" #include +#include #include #include "../mwbase/environment.hpp" @@ -24,8 +25,10 @@ namespace MWSound private: MWSound::MovieAudioDecoder* mDecoder; - void open(const std::string& fname) override; - void close() override; + void open(VFS::Path::NormalizedView fname) override { throw std::runtime_error("Method not implemented"); } + + void close() override {} + std::string getName() override; void getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) override; size_t read(char* buffer, size_t bytes) override; @@ -44,12 +47,19 @@ namespace MWSound size_t getSampleOffset() { +#if OPENMW_FFMPEG_5_OR_GREATER + ssize_t clock_delay = (mFrameSize - mFramePos) / mOutputChannelLayout.nb_channels +#else ssize_t clock_delay = (mFrameSize - mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif / av_get_bytes_per_sample(mOutputSampleFormat); return (size_t)(mAudioClock * mAudioContext->sample_rate) - clock_delay; } - std::string getStreamName() { return std::string(); } + std::string getStreamName() + { + return std::string(); + } private: // MovieAudioDecoder overrides @@ -92,12 +102,6 @@ namespace MWSound std::shared_ptr mDecoderBridge; }; - void MWSoundDecoderBridge::open(const std::string& fname) - { - throw std::runtime_error("Method not implemented"); - } - void MWSoundDecoderBridge::close() {} - std::string MWSoundDecoderBridge::getName() { return mDecoder->getStreamName(); diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 363a0d06b5..3c3d6fb26e 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -298,8 +298,6 @@ namespace MWSound std::atomic mIsFinished; - void updateAll(bool local); - OpenAL_SoundStream(const OpenAL_SoundStream& rhs); OpenAL_SoundStream& operator=(const OpenAL_SoundStream& rhs); @@ -610,9 +608,9 @@ namespace MWSound } } } - catch (std::exception&) + catch (const std::exception& e) { - Log(Debug::Error) << "Error updating stream \"" << mDecoder->getName() << "\""; + Log(Debug::Error) << "Error updating stream \"" << mDecoder->getName() << "\": " << e.what(); mIsFinished = true; } return !mIsFinished; @@ -1034,66 +1032,7 @@ namespace MWSound return ret; } - void OpenAL_Output::setHrtf(const std::string& hrtfname, HrtfMode hrtfmode) - { - if (!mDevice || !ALC.SOFT_HRTF) - { - Log(Debug::Info) << "HRTF extension not present"; - return; - } - - LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; - getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); - - LPALCRESETDEVICESOFT alcResetDeviceSOFT = nullptr; - getALCFunc(alcResetDeviceSOFT, mDevice, "alcResetDeviceSOFT"); - - std::vector attrs; - attrs.reserve(15); - - attrs.push_back(ALC_HRTF_SOFT); - attrs.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE - : hrtfmode == HrtfMode::Enable ? ALC_TRUE - : - /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); - if (!hrtfname.empty()) - { - ALCint index = -1; - ALCint num_hrtf; - alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); - for (ALCint i = 0; i < num_hrtf; ++i) - { - const ALCchar* entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); - if (hrtfname == entry) - { - index = i; - break; - } - } - - if (index < 0) - Log(Debug::Warning) << "Failed to find HRTF name \"" << hrtfname << "\", using default"; - else - { - attrs.push_back(ALC_HRTF_ID_SOFT); - attrs.push_back(index); - } - } - attrs.push_back(0); - alcResetDeviceSOFT(mDevice, attrs.data()); - - ALCint hrtf_state; - alcGetIntegerv(mDevice, ALC_HRTF_SOFT, 1, &hrtf_state); - if (!hrtf_state) - Log(Debug::Info) << "HRTF disabled"; - else - { - const ALCchar* hrtf = alcGetString(mDevice, ALC_HRTF_SPECIFIER_SOFT); - Log(Debug::Info) << "Enabled HRTF " << hrtf; - } - } - - std::pair OpenAL_Output::loadSound(const std::string& fname) + std::pair OpenAL_Output::loadSound(VFS::Path::NormalizedView fname) { getALError(); @@ -1104,7 +1043,7 @@ namespace MWSound try { DecoderPtr decoder = mManager.getDecoder(); - decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, decoder->mResourceMgr)); + decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, *decoder->mResourceMgr)); ChannelConfig chans; SampleType type; diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp index eed23ac659..b419038eab 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -7,6 +7,8 @@ #include #include +#include + #include "al.h" #include "alc.h" #include "alext.h" @@ -84,9 +86,8 @@ namespace MWSound void deinit() override; std::vector enumerateHrtf() override; - void setHrtf(const std::string& hrtfname, HrtfMode hrtfmode) override; - std::pair loadSound(const std::string& fname) override; + std::pair loadSound(VFS::Path::NormalizedView fname) override; size_t unloadSound(Sound_Handle data) override; bool playSound(Sound* sound, Sound_Handle data, float offset) override; diff --git a/apps/openmw/mwsound/regionsoundselector.cpp b/apps/openmw/mwsound/regionsoundselector.cpp index 8fda57596a..cb2ece7f8f 100644 --- a/apps/openmw/mwsound/regionsoundselector.cpp +++ b/apps/openmw/mwsound/regionsoundselector.cpp @@ -4,29 +4,18 @@ #include #include -#include -#include - #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" namespace MWSound { - namespace - { - int addChance(int result, const ESM::Region::SoundRef& v) - { - return result + v.mChance; - } - } - RegionSoundSelector::RegionSoundSelector() : mMinTimeBetweenSounds(Fallback::Map::getFloat("Weather_Minimum_Time_Between_Environmental_Sounds")) , mMaxTimeBetweenSounds(Fallback::Map::getFloat("Weather_Maximum_Time_Between_Environmental_Sounds")) { } - std::optional RegionSoundSelector::getNextRandom(float duration, const ESM::RefId& regionName) + ESM::RefId RegionSoundSelector::getNextRandom(float duration, const ESM::RefId& regionName) { mTimePassed += duration; @@ -37,40 +26,17 @@ namespace MWSound mTimeToNextEnvSound = mMinTimeBetweenSounds + (mMaxTimeBetweenSounds - mMinTimeBetweenSounds) * a; mTimePassed = 0; - if (mLastRegionName != regionName) - { - mLastRegionName = regionName; - mSumChance = 0; - } - const ESM::Region* const region - = MWBase::Environment::get().getESMStore()->get().search(mLastRegionName); + = MWBase::Environment::get().getESMStore()->get().search(regionName); if (region == nullptr) return {}; - if (mSumChance == 0) + for (const ESM::Region::SoundRef& sound : region->mSoundList) { - mSumChance = std::accumulate(region->mSoundList.begin(), region->mSoundList.end(), 0, addChance); - if (mSumChance == 0) - return {}; + if (Misc::Rng::roll0to99() < sound.mChance) + return sound.mSound; } - - const int r = Misc::Rng::rollDice(std::max(mSumChance, 100)); - int pos = 0; - - const auto isSelected = [&](const ESM::Region::SoundRef& sound) { - if (r - pos < sound.mChance) - return true; - pos += sound.mChance; - return false; - }; - - const auto it = std::find_if(region->mSoundList.begin(), region->mSoundList.end(), isSelected); - - if (it == region->mSoundList.end()) - return {}; - - return it->mSound; + return {}; } } diff --git a/apps/openmw/mwsound/regionsoundselector.hpp b/apps/openmw/mwsound/regionsoundselector.hpp index 1a9e6e450b..474e1afa06 100644 --- a/apps/openmw/mwsound/regionsoundselector.hpp +++ b/apps/openmw/mwsound/regionsoundselector.hpp @@ -2,27 +2,18 @@ #define GAME_SOUND_REGIONSOUNDSELECTOR_H #include -#include -#include - -namespace MWBase -{ - class World; -} namespace MWSound { class RegionSoundSelector { public: - std::optional getNextRandom(float duration, const ESM::RefId& regionName); + ESM::RefId getNextRandom(float duration, const ESM::RefId& regionName); RegionSoundSelector(); private: float mTimeToNextEnvSound = 0.0f; - int mSumChance = 0; - ESM::RefId mLastRegionName; float mTimePassed = 0.0; float mMinTimeBetweenSounds; float mMaxTimeBetweenSounds; diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp index d2645a1fe8..f28b268df2 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include @@ -37,11 +37,9 @@ namespace MWSound SoundBufferPool::SoundBufferPool(Sound_Output& output) : mOutput(&output) - , mBufferCacheMax(std::max(Settings::Manager::getInt("buffer cache max", "Sound"), 1) * 1024 * 1024) + , mBufferCacheMax(Settings::sound().mBufferCacheMax * 1024 * 1024) , mBufferCacheMin( - std::min(static_cast(std::max(Settings::Manager::getInt("buffer cache min", "Sound"), 1)) - * 1024 * 1024, - mBufferCacheMax)) + std::min(static_cast(Settings::sound().mBufferCacheMin) * 1024 * 1024, mBufferCacheMax)) { } @@ -185,9 +183,8 @@ namespace MWSound min = std::max(min, 1.0f); max = std::max(min, max); - Sound_Buffer& sfx - = mSoundBuffers.emplace_back(Misc::ResourceHelpers::correctSoundPath(sound.mSound), volume, min, max); - VFS::Path::normalizeFilenameInPlace(sfx.mResourceName); + Sound_Buffer& sfx = mSoundBuffers.emplace_back( + Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(sound.mSound)), volume, min, max); mBufferNameMap.emplace(soundId, &sfx); return &sfx; diff --git a/apps/openmw/mwsound/sound_buffer.hpp b/apps/openmw/mwsound/sound_buffer.hpp index 3bf734a4b6..7de6dab9ae 100644 --- a/apps/openmw/mwsound/sound_buffer.hpp +++ b/apps/openmw/mwsound/sound_buffer.hpp @@ -35,7 +35,7 @@ namespace MWSound { } - const std::string& getResourceName() const noexcept { return mResourceName; } + const VFS::Path::Normalized& getResourceName() const noexcept { return mResourceName; } Sound_Handle getHandle() const noexcept { return mHandle; } @@ -46,7 +46,7 @@ namespace MWSound float getMaxDist() const noexcept { return mMaxDist; } private: - std::string mResourceName; + VFS::Path::Normalized mResourceName; float mVolume; float mMinDist; float mMaxDist; diff --git a/apps/openmw/mwsound/sound_decoder.hpp b/apps/openmw/mwsound/sound_decoder.hpp index f6dcdb7032..17f9d28909 100644 --- a/apps/openmw/mwsound/sound_decoder.hpp +++ b/apps/openmw/mwsound/sound_decoder.hpp @@ -1,6 +1,8 @@ #ifndef GAME_SOUND_SOUND_DECODER_H #define GAME_SOUND_SOUND_DECODER_H +#include + #include #include @@ -36,7 +38,7 @@ namespace MWSound { const VFS::Manager* mResourceMgr; - virtual void open(const std::string& fname) = 0; + virtual void open(VFS::Path::NormalizedView fname) = 0; virtual void close() = 0; virtual std::string getName() = 0; diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/sound_output.hpp index d7c35fbbc6..5a77124985 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/sound_output.hpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #include "../mwbase/soundmanager.hpp" namespace MWSound @@ -19,19 +22,14 @@ namespace MWSound // An opaque handle for the implementation's sound instances. typedef void* Sound_Instance; - enum class HrtfMode - { - Disable, - Enable, - Auto - }; - enum Environment { Env_Normal, Env_Underwater }; + using HrtfMode = Settings::HrtfMode; + class Sound_Output { SoundManager& mManager; @@ -41,9 +39,8 @@ namespace MWSound virtual void deinit() = 0; virtual std::vector enumerateHrtf() = 0; - virtual void setHrtf(const std::string& hrtfname, HrtfMode hrtfmode) = 0; - virtual std::pair loadSound(const std::string& fname) = 0; + virtual std::pair loadSound(VFS::Path::NormalizedView fname) = 0; virtual size_t unloadSound(Sound_Handle data) = 0; virtual bool playSound(Sound* sound, Sound_Handle data, float offset) = 0; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index d5f1758f03..1d9a6d457c 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -10,11 +10,14 @@ #include #include #include +#include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" @@ -22,14 +25,14 @@ #include "../mwmechanics/actorutil.hpp" +#include "constants.hpp" +#include "ffmpeg_decoder.hpp" +#include "openal_output.hpp" #include "sound.hpp" #include "sound_buffer.hpp" #include "sound_decoder.hpp" #include "sound_output.hpp" -#include "ffmpeg_decoder.hpp" -#include "openal_output.hpp" - namespace MWSound { namespace @@ -71,6 +74,33 @@ namespace MWSound return 1.0; } + + // Gets the combined volume settings for the given sound type + float volumeFromType(Type type) + { + float volume = Settings::sound().mMasterVolume; + + switch (type) + { + case Type::Sfx: + volume *= Settings::sound().mSfxVolume; + break; + case Type::Voice: + volume *= Settings::sound().mVoiceVolume; + break; + case Type::Foot: + volume *= Settings::sound().mFootstepsVolume; + break; + case Type::Music: + volume *= Settings::sound().mMusicVolume; + break; + case Type::Movie: + case Type::Mask: + break; + } + + return volume; + } } // For combining PlayMode and Type flags @@ -84,6 +114,7 @@ namespace MWSound , mOutput(std::make_unique(*this)) , mWaterSoundUpdater(makeWaterSoundUpdaterSettings()) , mSoundBuffers(*mOutput) + , mMusicType(MWSound::MusicType::Normal) , mListenerUnderwater(false) , mListenerPos(0, 0, 0) , mListenerDir(1, 0, 0) @@ -101,12 +132,7 @@ namespace MWSound return; } - const std::string& hrtfname = Settings::Manager::getString("hrtf", "Sound"); - int hrtfstate = Settings::Manager::getInt("hrtf enable", "Sound"); - HrtfMode hrtfmode = hrtfstate < 0 ? HrtfMode::Auto : hrtfstate > 0 ? HrtfMode::Enable : HrtfMode::Disable; - - const std::string& devname = Settings::Manager::getString("device", "Sound"); - if (!mOutput->init(devname, hrtfname, hrtfmode)) + if (!mOutput->init(Settings::sound().mDevice, Settings::sound().mHrtf, Settings::sound().mHrtfEnable)) { Log(Debug::Error) << "Failed to initialize audio output, sound disabled"; return; @@ -131,15 +157,6 @@ namespace MWSound Log(Debug::Info) << stream.str(); } - - // TODO: dehardcode this - std::vector titleMusic; - std::string_view titlefile = "music/special/morrowind title.mp3"; - if (mVFS->exists(titlefile)) - titleMusic.emplace_back(titlefile); - else - Log(Debug::Warning) << "Title music not found"; - mMusicFiles["Title"] = titleMusic; } SoundManager::~SoundManager() @@ -155,12 +172,12 @@ namespace MWSound return std::make_shared(mVFS); } - DecoderPtr SoundManager::loadVoice(const std::string& voicefile) + DecoderPtr SoundManager::loadVoice(VFS::Path::NormalizedView voicefile) { try { DecoderPtr decoder = getDecoder(); - decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, decoder->mResourceMgr)); + decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, *decoder->mResourceMgr)); return decoder; } catch (std::exception& e) @@ -206,7 +223,7 @@ namespace MWSound params.mFlags = PlayMode::NoEnv | Type::Voice | Play_2D; return params; }()); - played = mOutput->streamSound(decoder, sound.get(), true); + played = mOutput->streamSound(std::move(decoder), sound.get(), true); } else { @@ -219,19 +236,13 @@ namespace MWSound params.mFlags = PlayMode::Normal | Type::Voice | Play_3D; return params; }()); - played = mOutput->streamSound3D(decoder, sound.get(), true); + played = mOutput->streamSound3D(std::move(decoder), sound.get(), true); } if (!played) return nullptr; return sound; } - // Gets the combined volume settings for the given sound type - float SoundManager::volumeFromType(Type type) const - { - return mVolumeSettings.getVolumeFromType(type); - } - void SoundManager::stopMusic() { if (mMusic) @@ -241,26 +252,25 @@ namespace MWSound } } - void SoundManager::streamMusicFull(const std::string& filename) + void SoundManager::streamMusicFull(VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; stopMusic(); - if (filename.empty()) + if (filename.value().empty()) return; - Log(Debug::Info) << "Playing " << filename; - mLastPlayedMusic = filename; + Log(Debug::Info) << "Playing \"" << filename << "\""; DecoderPtr decoder = getDecoder(); try { decoder->open(filename); } - catch (std::exception& e) + catch (const std::exception& e) { - Log(Debug::Error) << "Failed to load audio from " << filename << ": " << e.what(); + Log(Debug::Error) << "Failed to load audio from \"" << filename << "\": " << e.what(); return; } @@ -271,10 +281,10 @@ namespace MWSound params.mFlags = PlayMode::NoEnvNoScaling | Type::Music | Play_2D; return params; }()); - mOutput->streamSound(decoder, mMusic.get()); + mOutput->streamSound(std::move(decoder), mMusic.get()); } - void SoundManager::advanceMusic(const std::string& filename) + void SoundManager::advanceMusic(VFS::Path::NormalizedView filename, float fadeOut) { if (!isMusicPlaying()) { @@ -284,44 +294,7 @@ namespace MWSound mNextMusic = filename; - mMusic->setFadeout(1.f); - } - - void SoundManager::startRandomTitle() - { - const std::vector& filelist = mMusicFiles[mCurrentPlaylist]; - if (filelist.empty()) - { - advanceMusic(std::string()); - return; - } - - auto& tracklist = mMusicToPlay[mCurrentPlaylist]; - - // Do a Fisher-Yates shuffle - - // Repopulate if playlist is empty - if (tracklist.empty()) - { - tracklist.resize(filelist.size()); - std::iota(tracklist.begin(), tracklist.end(), 0); - } - - int i = Misc::Rng::rollDice(tracklist.size()); - - // Reshuffle if last played music is the same after a repopulation - if (filelist[tracklist[i]] == mLastPlayedMusic) - i = (i + 1) % tracklist.size(); - - // Remove music from list after advancing music - advanceMusic(filelist[tracklist[i]]); - tracklist[i] = tracklist.back(); - tracklist.pop_back(); - } - - void SoundManager::streamMusic(const std::string& filename) - { - advanceMusic("Music/" + filename); + mMusic->setFadeout(fadeOut); } bool SoundManager::isMusicPlaying() @@ -329,32 +302,17 @@ namespace MWSound return mMusic && mOutput->isStreamPlaying(mMusic.get()); } - void SoundManager::playPlaylist(const std::string& playlist) + void SoundManager::streamMusic(VFS::Path::NormalizedView filename, MusicType type, float fade) { - if (mCurrentPlaylist == playlist) + // Can not interrupt scripted music by built-in playlists + if (mMusicType == MusicType::MWScript && type != MusicType::MWScript) return; - if (mMusicFiles.find(playlist) == mMusicFiles.end()) - { - std::vector filelist; - for (const auto& name : mVFS->getRecursiveDirectoryIterator("Music/" + playlist + '/')) - filelist.push_back(name); - - mMusicFiles[playlist] = filelist; - } - - // No Battle music? Use Explore playlist - if (playlist == "Battle" && mMusicFiles[playlist].empty()) - { - playPlaylist("Explore"); - return; - } - - mCurrentPlaylist = playlist; - startRandomTitle(); + mMusicType = type; + advanceMusic(filename, fade); } - void SoundManager::say(const MWWorld::ConstPtr& ptr, const std::string& filename) + void SoundManager::say(const MWWorld::ConstPtr& ptr, VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; @@ -367,7 +325,7 @@ namespace MWSound const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); stopSay(ptr); - StreamPtr sound = playVoice(decoder, pos, (ptr == MWMechanics::getPlayer())); + StreamPtr sound = playVoice(std::move(decoder), pos, (ptr == MWMechanics::getPlayer())); if (!sound) return; @@ -386,7 +344,7 @@ namespace MWSound return 0.0f; } - void SoundManager::say(const std::string& filename) + void SoundManager::say(VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; @@ -396,7 +354,7 @@ namespace MWSound return; stopSay(MWWorld::ConstPtr()); - StreamPtr sound = playVoice(decoder, osg::Vec3f(), true); + StreamPtr sound = playVoice(std::move(decoder), osg::Vec3f(), true); if (!sound) return; @@ -869,13 +827,14 @@ namespace MWSound const MWWorld::ConstPtr player = world->getPlayerPtr(); auto cell = player.getCell()->getCell(); - if (!cell->isExterior()) + if (!cell->isExterior() && !cell->isQuasiExterior()) return; if (mCurrentRegionSound && mOutput->isSoundPlaying(mCurrentRegionSound)) return; - if (const auto next = mRegionSoundSelector.getNextRandom(duration, cell->getRegion())) - mCurrentRegionSound = playSound(*next, 1.0f, 1.0f); + ESM::RefId next = mRegionSoundSelector.getNextRandom(duration, cell->getRegion()); + if (!next.empty()) + mCurrentRegionSound = playSound(next, 1.0f, 1.0f); } void SoundManager::updateWaterSound() @@ -991,10 +950,6 @@ namespace MWSound duration = mTimePassed; mTimePassed = 0.0f; - // Make sure music is still playing - if (!isMusicPlaying() && !mCurrentPlaylist.empty()) - startRandomTitle(); - Environment env = Env_Normal; if (mListenerUnderwater) env = Env_Underwater; @@ -1110,11 +1065,13 @@ namespace MWSound if (!mMusic || !mMusic->updateFade(duration) || !mOutput->isStreamPlaying(mMusic.get())) { stopMusic(); - if (!mNextMusic.empty()) + if (!mNextMusic.value().empty()) { streamMusicFull(mNextMusic); - mNextMusic.clear(); + mNextMusic = VFS::Path::Normalized(); } + else + mMusicType = MusicType::Normal; } else { @@ -1127,8 +1084,18 @@ namespace MWSound if (!mOutput->isInitialized() || mPlaybackPaused) return; + MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); + bool isMainMenu = MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) + && state == MWBase::StateManager::State_NoGame; + + if (isMainMenu && !isMusicPlaying()) + { + if (mVFS->exists(MWSound::titleMusic)) + streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal); + } + updateSounds(duration); - if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (state != MWBase::StateManager::State_NoGame) { updateRegionSound(duration); updateWaterSound(); @@ -1137,8 +1104,6 @@ namespace MWSound void SoundManager::processChangedSettings(const Settings::CategorySettingVector& settings) { - mVolumeSettings.update(); - if (!mOutput->isInitialized()) return; mOutput->startUpdate(); @@ -1291,7 +1256,8 @@ namespace MWSound void SoundManager::clear() { - SoundManager::stopMusic(); + stopMusic(); + mMusicType = MusicType::Normal; for (SoundMap::value_type& snd : mActiveSounds) { diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 7453ce86f4..a5e5b2c45f 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include "../mwbase/soundmanager.hpp" @@ -16,7 +17,6 @@ #include "regionsoundselector.hpp" #include "sound_buffer.hpp" #include "type.hpp" -#include "volumesettings.hpp" #include "watersoundupdater.hpp" namespace VFS @@ -52,13 +52,6 @@ namespace MWSound std::unique_ptr mOutput; - // Caches available music tracks by - std::unordered_map> mMusicFiles; - std::unordered_map> mMusicToPlay; // A list with music files not yet played - std::string mLastPlayedMusic; // The music file that was last played - - VolumeSettings mVolumeSettings; - WaterSoundUpdater mWaterSoundUpdater; SoundBufferPool mSoundBuffers; @@ -93,7 +86,7 @@ namespace MWSound TrackList mActiveTracks; StreamPtr mMusic; - std::string mCurrentPlaylist; + MusicType mMusicType; bool mListenerUnderwater; osg::Vec3f mListenerPos; @@ -105,7 +98,7 @@ namespace MWSound Sound* mUnderwaterSound; Sound* mNearWaterSound; - std::string mNextMusic; + VFS::Path::Normalized mNextMusic; bool mPlaybackPaused; RegionSoundSelector mRegionSoundSelector; @@ -119,16 +112,15 @@ namespace MWSound Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound* sound); // returns a decoder to start streaming, or nullptr if the sound was not found - DecoderPtr loadVoice(const std::string& voicefile); + DecoderPtr loadVoice(VFS::Path::NormalizedView voicefile); SoundPtr getSoundRef(); StreamPtr getStreamRef(); StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f& pos, bool playlocal); - void streamMusicFull(const std::string& filename); - void advanceMusic(const std::string& filename); - void startRandomTitle(); + void streamMusicFull(VFS::Path::NormalizedView filename); + void advanceMusic(VFS::Path::NormalizedView filename, float fadeOut = 1.f); void cull3DSound(SoundBase* sound); @@ -144,8 +136,6 @@ namespace MWSound void updateWaterSound(); void updateMusic(float duration); - float volumeFromType(Type type) const; - enum class WaterSoundAction { DoNothing, @@ -173,26 +163,28 @@ namespace MWSound void processChangedSettings(const Settings::CategorySettingVector& settings) override; + bool isEnabled() const override { return mOutput->isInitialized(); } + ///< Returns true if sound system is enabled + void stopMusic() override; ///< Stops music if it's playing - void streamMusic(const std::string& filename) override; + MWSound::MusicType getMusicType() const override { return mMusicType; } + + void streamMusic(VFS::Path::NormalizedView filename, MWSound::MusicType type, float fade = 1.f) override; ///< Play a soundifle - /// \param filename name of a sound file in "Music/" in the data directory. + /// \param filename name of a sound file in the data directory. + /// \param type music type. + /// \param fade time in seconds to fade out current track before start this one. bool isMusicPlaying() override; ///< Returns true if music is playing - void playPlaylist(const std::string& playlist) override; - ///< Start playing music from the selected folder - /// \param name of the folder that contains the playlist - /// Title music playlist is predefined - - void say(const MWWorld::ConstPtr& reference, const std::string& filename) override; + void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) override; ///< Make an actor say some text. /// \param filename name of a sound file in the VFS - void say(const std::string& filename) override; + void say(VFS::Path::NormalizedView filename) override; ///< Say some text, without an actor ref /// \param filename name of a sound file in the VFS diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp deleted file mode 100644 index 4306bc5268..0000000000 --- a/apps/openmw/mwsound/volumesettings.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "volumesettings.hpp" - -#include - -#include - -namespace MWSound -{ - namespace - { - float clamp(float value) - { - return std::clamp(value, 0.f, 1.f); - } - } - - VolumeSettings::VolumeSettings() - : mMasterVolume(clamp(Settings::Manager::getFloat("master volume", "Sound"))) - , mSFXVolume(clamp(Settings::Manager::getFloat("sfx volume", "Sound"))) - , mMusicVolume(clamp(Settings::Manager::getFloat("music volume", "Sound"))) - , mVoiceVolume(clamp(Settings::Manager::getFloat("voice volume", "Sound"))) - , mFootstepsVolume(clamp(Settings::Manager::getFloat("footsteps volume", "Sound"))) - { - } - - float VolumeSettings::getVolumeFromType(Type type) const - { - float volume = mMasterVolume; - - switch (type) - { - case Type::Sfx: - volume *= mSFXVolume; - break; - case Type::Voice: - volume *= mVoiceVolume; - break; - case Type::Foot: - volume *= mFootstepsVolume; - break; - case Type::Music: - volume *= mMusicVolume; - break; - case Type::Movie: - case Type::Mask: - break; - } - - return volume; - } - - void VolumeSettings::update() - { - *this = VolumeSettings(); - } -} diff --git a/apps/openmw/mwsound/volumesettings.hpp b/apps/openmw/mwsound/volumesettings.hpp deleted file mode 100644 index 884a3e1bca..0000000000 --- a/apps/openmw/mwsound/volumesettings.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef GAME_SOUND_VOLUMESETTINGS_H -#define GAME_SOUND_VOLUMESETTINGS_H - -#include "type.hpp" - -namespace MWSound -{ - class VolumeSettings - { - public: - VolumeSettings(); - - float getVolumeFromType(Type type) const; - - void update(); - - private: - float mMasterVolume; - float mSFXVolume; - float mMusicVolume; - float mVoiceVolume; - float mFootstepsVolume; - }; -} - -#endif diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index 85f5087fe6..3c02311458 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -7,11 +7,11 @@ #include #include +#include #include #include -#include - #include +#include bool MWState::operator<(const Slot& left, const Slot& right) { @@ -84,8 +84,8 @@ void MWState::Character::addSlot(const ESM::SavedGame& profile) mSlots.push_back(slot); } -MWState::Character::Character(std::filesystem::path saves, const std::string& game) - : mPath(std::move(saves)) +MWState::Character::Character(const std::filesystem::path& saves, const std::string& game) + : mPath(saves) { if (!std::filesystem::is_directory(mPath)) { @@ -99,9 +99,11 @@ MWState::Character::Character(std::filesystem::path saves, const std::string& ga { addSlot(iter, game); } - catch (...) + catch (const std::exception& e) { - } // ignoring bad saved game files for now + Log(Debug::Warning) << "Failed to add slot for game \"" << game << "\" save " << iter << ": " + << e.what(); + } } std::sort(mSlots.begin(), mSlots.end()); @@ -132,9 +134,9 @@ const MWState::Slot* MWState::Character::createSlot(const ESM::SavedGame& profil void MWState::Character::deleteSlot(const Slot* slot) { - int index = slot - mSlots.data(); + std::ptrdiff_t index = slot - mSlots.data(); - if (index < 0 || index >= static_cast(mSlots.size())) + if (index < 0 || static_cast(index) >= mSlots.size()) { // sanity check; not entirely reliable throw std::logic_error("slot not found"); @@ -147,9 +149,9 @@ void MWState::Character::deleteSlot(const Slot* slot) const MWState::Slot* MWState::Character::updateSlot(const Slot* slot, const ESM::SavedGame& profile) { - int index = slot - mSlots.data(); + std::ptrdiff_t index = slot - mSlots.data(); - if (index < 0 || index >= static_cast(mSlots.size())) + if (index < 0 || static_cast(index) >= mSlots.size()) { // sanity check; not entirely reliable throw std::logic_error("slot not found"); diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 7b9eba2fee..3c68d9f490 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -32,7 +32,7 @@ namespace MWState void addSlot(const ESM::SavedGame& profile); public: - Character(std::filesystem::path saves, const std::string& game); + Character(const std::filesystem::path& saves, const std::string& game); void cleanup(); ///< Delete the directory we used, if it is empty diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 50fd123b4f..9e292a3eee 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -2,6 +2,8 @@ #include +#include + #include #include @@ -14,7 +16,8 @@ #include #include -#include +#include +#include #include @@ -59,13 +62,19 @@ void MWState::StateManager::cleanup(bool force) MWBase::Environment::get().getInputManager()->clear(); MWBase::Environment::get().getMechanicsManager()->clear(); - mState = State_NoGame; mCharacterManager.setCurrentCharacter(nullptr); mTimePlayed = 0; - + mLastSavegame.clear(); MWMechanics::CreatureStats::cleanup(); + + mState = State_NoGame; + MWBase::Environment::get().getLuaManager()->noGame(); + } + else + { + // TODO: do we need this cleanup? + MWBase::Environment::get().getLuaManager()->clear(); } - MWBase::Environment::get().getLuaManager()->clear(); } std::map MWState::StateManager::buildContentFileIndexMap(const ESM::ESMReader& reader) const @@ -78,10 +87,8 @@ std::map MWState::StateManager::buildContentFileIndexMap(const ESM::ES for (int iPrev = 0; iPrev < static_cast(prev.size()); ++iPrev) { - std::string id = Misc::StringUtils::lowerCase(prev[iPrev].name); - for (int iCurrent = 0; iCurrent < static_cast(current.size()); ++iCurrent) - if (id == Misc::StringUtils::lowerCase(current[iCurrent])) + if (Misc::StringUtils::ciEqual(prev[iPrev].name, current[iCurrent])) { map.insert(std::make_pair(iPrev, iCurrent)); break; @@ -117,14 +124,27 @@ void MWState::StateManager::askLoadRecent() if (!mAskLoadRecent) { - const MWState::Character* character = getCurrentCharacter(); - if (!character || character->begin() == character->end()) // no saves + if (mLastSavegame.empty()) // no saves { MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } else { - MWState::Slot lastSave = *character->begin(); + std::string saveName = Files::pathToUnicodeString(mLastSavegame.filename()); + // Assume the last saved game belongs to the current character's slot list. + const Character* character = getCurrentCharacter(); + if (character) + { + for (const auto& slot : *character) + { + if (slot.mPath == mLastSavegame) + { + saveName = slot.mProfile.mDescription; + break; + } + } + } + std::vector buttons; buttons.emplace_back("#{Interface:Yes}"); buttons.emplace_back("#{Interface:No}"); @@ -132,7 +152,7 @@ void MWState::StateManager::askLoadRecent() = MWBase::Environment::get().getL10nManager()->getMessage("OMWEngine", "AskLoadLastSave"); std::string_view tag = "%s"; size_t pos = message.find(tag); - message.replace(pos, tag.length(), lastSave.mProfile.mDescription); + message.replace(pos, tag.length(), saveName); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); mAskLoadRecent = true; } @@ -155,10 +175,10 @@ void MWState::StateManager::newGame(bool bypass) { Log(Debug::Info) << "Starting a new game"; MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); - MWBase::Environment::get().getLuaManager()->newGameStarted(); MWBase::Environment::get().getWorld()->startNewGame(bypass); mState = State_Running; + MWBase::Environment::get().getLuaManager()->gameLoaded(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); @@ -182,11 +202,13 @@ void MWState::StateManager::newGame(bool bypass) void MWState::StateManager::endGame() { mState = State_Ended; + MWBase::Environment::get().getLuaManager()->gameEnded(); } void MWState::StateManager::resumeGame() { mState = State_Running; + MWBase::Environment::get().getLuaManager()->gameLoaded(); } void MWState::StateManager::saveGame(std::string_view description, const Slot* slot) @@ -227,10 +249,15 @@ void MWState::StateManager::saveGame(std::string_view description, const Slot* s else profile.mPlayerClassId = classId; + const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + profile.mPlayerCellName = world.getCellName(); profile.mInGameTime = world.getTimeManager()->getEpochTimeStamp(); profile.mTimePlayed = mTimePlayed; profile.mDescription = description; + profile.mCurrentDay = world.getTimeManager()->getTimeStamp().getDay(); + profile.mCurrentHealth = stats.getHealth().getCurrent(); + profile.mMaximumHealth = stats.getHealth().getModified(); Log(Debug::Info) << "Making a screenshot for saved game '" << description << "'"; writeScreenshot(profile.mScreenshot); @@ -314,8 +341,8 @@ void MWState::StateManager::saveGame(std::string_view description, const Slot* s if (filestream.fail()) throw std::runtime_error("Write operation failed (file stream)"); - Settings::Manager::setString( - "character", "Saves", Files::pathToUnicodeString(slot->mPath.parent_path().filename())); + Settings::saves().mCharacter.set(Files::pathToUnicodeString(slot->mPath.parent_path().filename())); + mLastSavegame = slot->mPath; const auto finish = std::chrono::steady_clock::now(); @@ -354,12 +381,8 @@ void MWState::StateManager::quickSave(std::string name) return; } - int maxSaves = Settings::Manager::getInt("max quicksaves", "Saves"); - if (maxSaves < 1) - maxSaves = 1; - Character* currentCharacter = getCurrentCharacter(); // Get current character - QuickSaveManager saveFinder = QuickSaveManager(name, maxSaves); + QuickSaveManager saveFinder(name, Settings::saves().mMaxQuicksaves); if (currentCharacter) { @@ -393,9 +416,38 @@ void MWState::StateManager::loadGame(const std::filesystem::path& filepath) loadGame(character, filepath); } -struct VersionMismatchError : public std::runtime_error +struct SaveFormatVersionError : public std::exception { - using std::runtime_error::runtime_error; + using std::exception::exception; + + SaveFormatVersionError(ESM::FormatVersion savegameFormat, const std::string& message) + : mSavegameFormat(savegameFormat) + , mErrorMessage(message) + { + } + + const char* what() const noexcept override { return mErrorMessage.c_str(); } + ESM::FormatVersion getFormatVersion() const { return mSavegameFormat; } + +protected: + ESM::FormatVersion mSavegameFormat = ESM::DefaultFormatVersion; + std::string mErrorMessage; +}; + +struct SaveVersionTooOldError : SaveFormatVersionError +{ + SaveVersionTooOldError(ESM::FormatVersion savegameFormat) + : SaveFormatVersionError(savegameFormat, "format version " + std::to_string(savegameFormat) + " is too old") + { + } +}; + +struct SaveVersionTooNewError : SaveFormatVersionError +{ + SaveVersionTooNewError(ESM::FormatVersion savegameFormat) + : SaveFormatVersionError(savegameFormat, "format version " + std::to_string(savegameFormat) + " is too new") + { + } }; void MWState::StateManager::loadGame(const Character* character, const std::filesystem::path& filepath) @@ -409,10 +461,11 @@ void MWState::StateManager::loadGame(const Character* character, const std::file ESM::ESMReader reader; reader.open(filepath); - if (reader.getFormatVersion() > ESM::CurrentSaveGameFormatVersion) - throw VersionMismatchError( - "This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade " - "to the newest OpenMW version to load this file."); + ESM::FormatVersion version = reader.getFormatVersion(); + if (version > ESM::CurrentSaveGameFormatVersion) + throw SaveVersionTooNewError(version); + else if (version < ESM::MinSupportedSaveGameFormatVersion) + throw SaveVersionTooOldError(version); std::map contentFileMap = buildContentFileIndexMap(reader); reader.setContentFileMapping(&contentFileMap); @@ -440,7 +493,9 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { ESM::SavedGame profile; profile.load(reader); - if (!verifyProfile(profile)) + const auto& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); + auto missingFiles = profile.getMissingContentFiles(selectedContentFiles); + if (!missingFiles.empty() && !confirmLoading(missingFiles)) { cleanup(true); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); @@ -453,7 +508,6 @@ void MWState::StateManager::loadGame(const Character* character, const std::file break; case ESM::REC_JOUR: - case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: MWBase::Environment::get().getJournal()->readRecord(reader, n.toInt()); @@ -486,6 +540,7 @@ void MWState::StateManager::loadGame(const Character* character, const std::file case ESM::REC_ENAB: case ESM::REC_LEVC: case ESM::REC_LEVI: + case ESM::REC_LIGH: case ESM::REC_CREA: case ESM::REC_CONT: case ESM::REC_RAND: @@ -542,8 +597,8 @@ void MWState::StateManager::loadGame(const Character* character, const std::file mState = State_Running; if (character) - Settings::Manager::setString( - "character", "Saves", Files::pathToUnicodeString(character->getPath().filename())); + Settings::saves().mCharacter.set(Files::pathToUnicodeString(character->getPath().filename())); + mLastSavegame = filepath; MWBase::Environment::get().getWindowManager()->setNewGame(false); MWBase::Environment::get().getWorld()->saveLoaded(); @@ -551,6 +606,7 @@ void MWState::StateManager::loadGame(const Character* character, const std::file MWBase::Environment::get().getWorld()->renderPlayer(); MWBase::Environment::get().getWindowManager()->updatePlayer(); MWBase::Environment::get().getMechanicsManager()->playerLoaded(); + MWBase::Environment::get().getWorld()->toggleVanityMode(false); if (firstPersonCam != MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(); @@ -570,7 +626,7 @@ void MWState::StateManager::loadGame(const Character* character, const std::file Log(Debug::Warning) << "Player character's cell no longer exists, changing to the default cell"; ESM::ExteriorCellLocation cellIndex(0, 0, ESM::Cell::sDefaultWorldspaceId); MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getExterior(cellIndex); - osg::Vec2 posFromIndex = ESM::indexToPosition(cellIndex, false); + const osg::Vec2f posFromIndex = ESM::indexToPosition(cellIndex, false); ESM::Position pos; pos.pos[0] = posFromIndex.x(); pos.pos[1] = posFromIndex.y(); @@ -593,40 +649,71 @@ void MWState::StateManager::loadGame(const Character* character, const std::file MWBase::Environment::get().getLuaManager()->gameLoaded(); } + catch (const SaveVersionTooNewError& e) + { + std::string error = "#{OMWEngine:LoadingRequiresNewVersionError}"; + printSavegameFormatError(e.what(), error); + } + catch (const SaveVersionTooOldError& e) + { + const char* release; + // Report the last version still capable of reading this save + if (e.getFormatVersion() <= ESM::OpenMW0_48SaveGameFormatVersion) + release = "OpenMW 0.48.0"; + else + { + // Insert additional else if statements above to cover future releases + static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49SaveGameFormatVersion); + release = "OpenMW 0.49.0"; + } + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + std::string error = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); + printSavegameFormatError(e.what(), error); + } catch (const std::exception& e) { - Log(Debug::Error) << "Failed to load saved game: " << e.what(); - - cleanup(true); - - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); - - std::vector buttons; - buttons.emplace_back("#{Interface:OK}"); - - std::string error; - if (typeid(e) == typeid(VersionMismatchError)) - error = "#{OMWEngine:LoadingFailed}: #{OMWEngine:LoadingRequiresNewVersionError}"; - else - error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); - - MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error, buttons); + std::string error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); + printSavegameFormatError(e.what(), error); } } +void MWState::StateManager::printSavegameFormatError( + const std::string& exceptionText, const std::string& messageBoxText) +{ + Log(Debug::Error) << "Failed to load saved game: " << exceptionText; + + cleanup(true); + + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); + + std::vector buttons; + buttons.emplace_back("#{Interface:OK}"); + + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(messageBoxText, buttons); +} + void MWState::StateManager::quickLoad() { if (Character* currentCharacter = getCurrentCharacter()) { if (currentCharacter->begin() == currentCharacter->end()) return; - loadGame(currentCharacter, currentCharacter->begin()->mPath); // Get newest save + // use requestLoad, otherwise we can crash by loading during the wrong part of the frame + requestLoad(currentCharacter->begin()->mPath); } } void MWState::StateManager::deleteGame(const MWState::Character* character, const MWState::Slot* slot) { + const std::filesystem::path savePath = slot->mPath; mCharacterManager.deleteSlot(character, slot); + if (mLastSavegame == savePath) + { + if (character->begin() != character->end()) + mLastSavegame = character->begin()->mPath; + else + mLastSavegame.clear(); + } } MWState::Character* MWState::StateManager::getCurrentCharacter() @@ -657,9 +744,9 @@ void MWState::StateManager::update(float duration) { mAskLoadRecent = false; // Load last saved game for current character - - MWState::Slot lastSave = *curCharacter->begin(); - loadGame(curCharacter, lastSave.mPath); + // loadGame resets the game state along with mLastSavegame so we want to preserve it + const std::filesystem::path filePath = std::move(mLastSavegame); + loadGame(curCharacter, filePath); } else if (iButton == 1) { @@ -667,32 +754,80 @@ void MWState::StateManager::update(float duration) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } } + + if (mNewGameRequest) + { + newGame(); + mNewGameRequest = false; + } + + if (mLoadRequest) + { + loadGame(*mLoadRequest); + mLoadRequest = std::nullopt; + } } -bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const +bool MWState::StateManager::confirmLoading(const std::vector& missingFiles) const { - const std::vector& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - bool notFound = false; - for (const std::string& contentFile : profile.mContentFiles) + std::ostringstream stream; + for (auto& contentFile : missingFiles) { - if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile) - == selectedContentFiles.end()) + Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; + stream << contentFile << "\n"; + } + + auto fullList = stream.str(); + if (!fullList.empty()) + fullList.pop_back(); + + constexpr size_t missingPluginsDisplayLimit = 12; + + std::vector buttons; + buttons.emplace_back("#{Interface:Yes}"); + buttons.emplace_back("#{Interface:Copy}"); + buttons.emplace_back("#{Interface:No}"); + std::string message = "#{OMWEngine:MissingContentFilesConfirmation}"; + + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + message += l10n->formatMessage("MissingContentFilesList", { "files" }, { static_cast(missingFiles.size()) }); + auto cappedSize = std::min(missingFiles.size(), missingPluginsDisplayLimit); + if (cappedSize == missingFiles.size()) + { + message += fullList; + } + else + { + for (size_t i = 0; i < cappedSize - 1; ++i) { - Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; - notFound = true; + message += missingFiles[i]; + message += "\n"; } + + message += "..."; } - if (notFound) + + message + += l10n->formatMessage("MissingContentFilesListCopy", { "files" }, { static_cast(missingFiles.size()) }); + + int selectedButton = -1; + while (true) { - std::vector buttons; - buttons.emplace_back("#{Interface:Yes}"); - buttons.emplace_back("#{Interface:No}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox( - "#{OMWEngine:MissingContentFilesConfirmation}", buttons, true); - int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - if (selectedButton == 1 || selectedButton == -1) - return false; + auto windowManager = MWBase::Environment::get().getWindowManager(); + windowManager->interactiveMessageBox(message, buttons, true, selectedButton); + selectedButton = windowManager->readPressedButton(); + if (selectedButton == 0) + break; + + if (selectedButton == 1) + { + SDL_SetClipboardText(fullList.c_str()); + continue; + } + + return false; } + return true; } diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index df62ca7ebf..b08584a817 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -14,14 +14,19 @@ namespace MWState { bool mQuitRequest; bool mAskLoadRecent; + bool mNewGameRequest = false; + std::optional mLoadRequest; State mState; CharacterManager mCharacterManager; double mTimePlayed; + std::filesystem::path mLastSavegame; private: void cleanup(bool force = false); - bool verifyProfile(const ESM::SavedGame& profile) const; + void printSavegameFormatError(const std::string& exceptionText, const std::string& messageBoxText); + + bool confirmLoading(const std::vector& missingFiles) const; void writeScreenshot(std::vector& imageData) const; @@ -36,6 +41,9 @@ namespace MWState void askLoadRecent() override; + void requestNewGame() override { mNewGameRequest = true; } + void requestLoad(const std::filesystem::path& filepath) override { mLoadRequest = filepath; } + State getState() const override; void newGame(bool bypass = false) override; diff --git a/apps/openmw/mwworld/actioneat.cpp b/apps/openmw/mwworld/actioneat.cpp index b77fe146ef..c67502bdee 100644 --- a/apps/openmw/mwworld/actioneat.cpp +++ b/apps/openmw/mwworld/actioneat.cpp @@ -11,7 +11,7 @@ namespace MWWorld void ActionEat::executeImp(const Ptr& actor) { if (actor.getClass().consume(getTarget(), actor) && actor == MWMechanics::getPlayer()) - actor.getClass().skillUsageSucceeded(actor, ESM::Skill::Alchemy, 1); + actor.getClass().skillUsageSucceeded(actor, ESM::Skill::Alchemy, ESM::Skill::Alchemy_UseIngredient); } ActionEat::ActionEat(const MWWorld::Ptr& object) diff --git a/apps/openmw/mwworld/actionharvest.cpp b/apps/openmw/mwworld/actionharvest.cpp index 708260d395..30f316c2db 100644 --- a/apps/openmw/mwworld/actionharvest.cpp +++ b/apps/openmw/mwworld/actionharvest.cpp @@ -37,7 +37,7 @@ namespace MWWorld if (!it->getClass().showsInInventory(*it)) continue; - int itemCount = it->getRefData().getCount(); + int itemCount = it->getCellRef().getCount(); // Note: it is important to check for crime before move an item from container. Otherwise owner check will // not work for a last item in the container - empty harvested containers are considered as "allowed to // use". diff --git a/apps/openmw/mwworld/actionread.cpp b/apps/openmw/mwworld/actionread.cpp index 3f48920dc0..b6014b834c 100644 --- a/apps/openmw/mwworld/actionread.cpp +++ b/apps/openmw/mwworld/actionread.cpp @@ -5,6 +5,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" @@ -22,15 +23,15 @@ namespace MWWorld void ActionRead::executeImp(const MWWorld::Ptr& actor) { - - if (actor != MWMechanics::getPlayer()) + const MWWorld::Ptr player = MWMechanics::getPlayer(); + if (actor != player && getTarget().getContainerStore() != nullptr) return; // Ensure we're not in combat if (MWMechanics::isPlayerInCombat() // Reading in combat is still allowed if the scroll/book is not in the player inventory yet // (since otherwise, there would be no way to pick it up) - && getTarget().getContainerStore() == &actor.getClass().getContainerStore(actor)) + && getTarget().getContainerStore() == &player.getClass().getContainerStore(player)) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage4}"); return; @@ -43,18 +44,13 @@ namespace MWWorld else MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Book, getTarget()); - MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats(actor); + MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); // Skill gain from books ESM::RefId skill = ESM::Skill::indexToRefId(ref->mBase->mData.mSkillId); if (!skill.empty() && !npcStats.hasBeenUsed(ref->mBase->mId)) { - MWWorld::LiveCellRef* playerRef = actor.get(); - - const ESM::Class* class_ - = MWBase::Environment::get().getESMStore()->get().find(playerRef->mBase->mClass); - - npcStats.increaseSkill(skill, *class_, true, true); + MWBase::Environment::get().getLuaManager()->skillLevelUp(player, skill, "book"); npcStats.flagAsUsed(ref->mBase->mId); } diff --git a/apps/openmw/mwworld/actiontake.cpp b/apps/openmw/mwworld/actiontake.cpp index 83d6e729c9..1f37499e8f 100644 --- a/apps/openmw/mwworld/actiontake.cpp +++ b/apps/openmw/mwworld/actiontake.cpp @@ -30,10 +30,12 @@ namespace MWWorld } } - MWBase::Environment::get().getMechanicsManager()->itemTaken( - actor, getTarget(), MWWorld::Ptr(), getTarget().getRefData().getCount()); - MWWorld::Ptr newitem - = *actor.getClass().getContainerStore(actor).add(getTarget(), getTarget().getRefData().getCount()); + int count = getTarget().getCellRef().getCount(); + if (getTarget().getClass().isGold(getTarget())) + count *= getTarget().getClass().getValue(getTarget()); + + MWBase::Environment::get().getMechanicsManager()->itemTaken(actor, getTarget(), MWWorld::Ptr(), count); + MWWorld::Ptr newitem = *actor.getClass().getContainerStore(actor).add(getTarget(), count); MWBase::Environment::get().getWorld()->deleteObject(getTarget()); setTarget(newitem); } diff --git a/apps/openmw/mwworld/cell.cpp b/apps/openmw/mwworld/cell.cpp index 56afc104cf..1bc58d89c3 100644 --- a/apps/openmw/mwworld/cell.cpp +++ b/apps/openmw/mwworld/cell.cpp @@ -100,18 +100,7 @@ namespace MWWorld mWaterHeight = -1.f; mHasWater = true; } - } - - ESM::RefId Cell::getWorldSpace() const - { - if (isExterior()) - return mParent; else - return mId; - } - - ESM::ExteriorCellLocation Cell::getExteriorCellLocation() const - { - return { mGridPos.x(), mGridPos.y(), getWorldSpace() }; + mGridPos = {}; } } diff --git a/apps/openmw/mwworld/cell.hpp b/apps/openmw/mwworld/cell.hpp index 1fa088eac4..c9586a01fa 100644 --- a/apps/openmw/mwworld/cell.hpp +++ b/apps/openmw/mwworld/cell.hpp @@ -4,8 +4,8 @@ #include #include +#include #include -#include namespace ESM { @@ -48,8 +48,12 @@ namespace MWWorld const MoodData& getMood() const { return mMood; } float getWaterHeight() const { return mWaterHeight; } const ESM::RefId& getId() const { return mId; } - ESM::RefId getWorldSpace() const; - ESM::ExteriorCellLocation getExteriorCellLocation() const; + ESM::RefId getWorldSpace() const { return mIsExterior ? mParent : mId; } + + ESM::ExteriorCellLocation getExteriorCellLocation() const + { + return ESM::ExteriorCellLocation(mGridPos.x(), mGridPos.y(), getWorldSpace()); + } private: bool mIsExterior; diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 7da5e8f848..5540982f26 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -1,14 +1,19 @@ #include "cellpreloader.hpp" -#include +#include #include #include +#include + +#include #include #include #include #include +#include #include +#include #include #include #include @@ -23,50 +28,38 @@ #include "cellstore.hpp" #include "class.hpp" -namespace -{ - template - bool contains(const std::vector& container, const Contained& contained, - float tolerance) - { - for (const auto& pos : contained) - { - bool found = false; - for (const auto& pos2 : container) - { - if ((pos.first - pos2.first).length2() < tolerance * tolerance && pos.second == pos2.second) - { - found = true; - break; - } - } - if (!found) - return false; - } - return true; - } -} - namespace MWWorld { + namespace + { + bool contains(std::span positions, const PositionCellGrid& contained, float tolerance) + { + const float squaredTolerance = tolerance * tolerance; + const auto predicate = [&](const PositionCellGrid& v) { + return (contained.mPosition - v.mPosition).length2() < squaredTolerance + && contained.mCellBounds == v.mCellBounds; + }; + return std::ranges::any_of(positions, predicate); + } + + bool contains( + std::span container, std::span contained, float tolerance) + { + const auto predicate = [&](const PositionCellGrid& v) { return contains(container, v, tolerance); }; + return std::ranges::all_of(contained, predicate); + } + } struct ListModelsVisitor { - ListModelsVisitor(std::vector& out) - : mOut(out) - { - } - - virtual bool operator()(const MWWorld::Ptr& ptr) + bool operator()(const MWWorld::ConstPtr& ptr) { ptr.getClass().getModelsToPreload(ptr, mOut); return true; } - virtual ~ListModelsVisitor() = default; - - std::vector& mOut; + std::vector& mOut; }; /// Worker thread item: preload models in a cell. @@ -74,12 +67,12 @@ namespace MWWorld { public: /// Constructor to be called from the main thread. - PreloadItem(MWWorld::CellStore* cell, Resource::SceneManager* sceneManager, + explicit PreloadItem(MWWorld::CellStore* cell, Resource::SceneManager* sceneManager, Resource::BulletShapeManager* bulletShapeManager, Resource::KeyframeManager* keyframeManager, Terrain::World* terrain, MWRender::LandManager* landManager, bool preloadInstances) : mIsExterior(cell->getCell()->isExterior()) - , mX(cell->getCell()->getGridX()) - , mY(cell->getCell()->getGridY()) + , mCellLocation(cell->getCell()->getExteriorCellLocation()) + , mCellId(cell->getCell()->getId()) , mSceneManager(sceneManager) , mBulletShapeManager(bulletShapeManager) , mKeyframeManager(keyframeManager) @@ -90,8 +83,8 @@ namespace MWWorld { mTerrainView = mTerrain->createView(); - ListModelsVisitor visitor(mMeshes); - cell->forEach(visitor); + ListModelsVisitor visitor{ mMeshes }; + cell->forEachConst(visitor); } void abort() override { mAbort = true; } @@ -103,59 +96,59 @@ namespace MWWorld { try { - mTerrain->cacheCell(mTerrainView.get(), mX, mY); - mPreloadedObjects.insert( - mLandManager->getLand(ESM::ExteriorCellLocation(mX, mY, ESM::Cell::sDefaultWorldspaceId))); + mTerrain->cacheCell(mTerrainView.get(), mCellLocation.mX, mCellLocation.mY); + mPreloadedObjects.insert(mLandManager->getLand(mCellLocation)); } - catch (std::exception&) + catch (const std::exception& e) { + Log(Debug::Warning) << "Failed to cache terrain for exterior cell " << mCellLocation << ": " + << e.what(); } } - for (std::string& mesh : mMeshes) + VFS::Path::Normalized mesh; + VFS::Path::Normalized kfname; + for (std::string_view path : mMeshes) { if (mAbort) break; try { - mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mSceneManager->getVFS()); + const VFS::Manager& vfs = *mSceneManager->getVFS(); + mesh = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(path)); + mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, &vfs); - size_t slashpos = mesh.find_last_of("/\\"); - if (slashpos != std::string::npos && slashpos != mesh.size() - 1) + if (!vfs.exists(mesh)) + continue; + + if (Misc::getFileName(mesh).starts_with('x') && Misc::getFileExtension(mesh) == "nif") { - Misc::StringUtils::lowerCaseInPlace(mesh); - if (mesh[slashpos + 1] == 'x') - { - if (mesh.size() > 4 && mesh.ends_with(".nif")) - { - std::string kfname = mesh; - kfname.replace(kfname.size() - 4, 4, ".kf"); - if (mSceneManager->getVFS()->exists(kfname)) - mPreloadedObjects.insert(mKeyframeManager->get(kfname)); - } - } + kfname = mesh; + kfname.changeExtension("kf"); + if (vfs.exists(kfname)) + mPreloadedObjects.insert(mKeyframeManager->get(kfname)); } + mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh)); } - catch (std::exception&) + catch (const std::exception& e) { - // ignore error for now, would spam the log too much - // error will be shown when visiting the cell + Log(Debug::Warning) << "Failed to preload mesh \"" << path << "\" from cell " << mCellId << ": " + << e.what(); } } } private: - typedef std::vector MeshList; bool mIsExterior; - int mX; - int mY; - MeshList mMeshes; + ESM::ExteriorCellLocation mCellLocation; + ESM::RefId mCellId; + std::vector mMeshes; Resource::SceneManager* mSceneManager; Resource::BulletShapeManager* mBulletShapeManager; Resource::KeyframeManager* mKeyframeManager; @@ -174,12 +167,12 @@ namespace MWWorld class TerrainPreloadItem : public SceneUtil::WorkItem { public: - TerrainPreloadItem(const std::vector>& views, Terrain::World* world, - const std::vector& preloadPositions) + explicit TerrainPreloadItem(const std::vector>& views, Terrain::World* world, + std::span preloadPositions) : mAbort(false) , mTerrainViews(views) , mWorld(world) - , mPreloadPositions(preloadPositions) + , mPreloadPositions(preloadPositions.begin(), preloadPositions.end()) { } @@ -188,8 +181,8 @@ namespace MWWorld for (unsigned int i = 0; i < mTerrainViews.size() && i < mPreloadPositions.size() && !mAbort; ++i) { mTerrainViews[i]->reset(); - mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, - mLoadingReporter); + mWorld->preload(mTerrainViews[i], mPreloadPositions[i].mPosition, mPreloadPositions[i].mCellBounds, + mAbort, mLoadingReporter); } mLoadingReporter.complete(); } @@ -202,7 +195,7 @@ namespace MWWorld std::atomic mAbort; std::vector> mTerrainViews; Terrain::World* mWorld; - std::vector mPreloadPositions; + std::vector mPreloadPositions; Loading::Reporter mLoadingReporter; }; @@ -230,8 +223,6 @@ namespace MWWorld , mTerrain(terrain) , mLandManager(landManager) , mExpiryDelay(0.0) - , mMinCacheSize(0) - , mMaxCacheSize(0) , mPreloadInstances(true) , mLastResourceCacheUpdate(0.0) , mLoadedTerrainTimestamp(0.0) @@ -283,6 +274,7 @@ namespace MWWorld { oldestCell->second.mWorkItem->abort(); mPreloadCells.erase(oldestCell); + ++mEvicted; } else return; @@ -292,7 +284,8 @@ namespace MWWorld mResourceSystem->getKeyframeManager(), mTerrain, mLandManager, mPreloadInstances)); mWorkQueue->addWorkItem(item); - mPreloadCells[&cell] = PreloadEntry(timestamp, item); + mPreloadCells.emplace(&cell, PreloadEntry(timestamp, item)); + ++mAdded; } void CellPreloader::notifyLoaded(CellStore* cell) @@ -307,6 +300,7 @@ namespace MWWorld } mPreloadCells.erase(found); + ++mLoaded; } } @@ -336,6 +330,7 @@ namespace MWWorld it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); + ++mExpired; } else ++it; @@ -362,26 +357,11 @@ namespace MWWorld mExpiryDelay = expiryDelay; } - void CellPreloader::setMinCacheSize(unsigned int num) - { - mMinCacheSize = num; - } - - void CellPreloader::setMaxCacheSize(unsigned int num) - { - mMaxCacheSize = num; - } - void CellPreloader::setPreloadInstances(bool preload) { mPreloadInstances = preload; } - unsigned int CellPreloader::getMaxCacheSize() const - { - return mMaxCacheSize; - } - void CellPreloader::setWorkQueue(osg::ref_ptr workQueue) { mWorkQueue = workQueue; @@ -393,19 +373,19 @@ namespace MWWorld mTerrainPreloadItem->wait(listener); } - void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid* exceptPos) + void CellPreloader::abortTerrainPreloadExcept(const PositionCellGrid* exceptPos) { - if (exceptPos && contains(mTerrainPreloadPositions, std::array{ *exceptPos }, Constants::CellSizeInUnits)) + if (exceptPos != nullptr && contains(mTerrainPreloadPositions, *exceptPos, Constants::CellSizeInUnits)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); mTerrainPreloadItem->waitTillDone(); } - setTerrainPreloadPositions(std::vector()); + setTerrainPreloadPositions({}); } - void CellPreloader::setTerrainPreloadPositions(const std::vector& positions) + void CellPreloader::setTerrainPreloadPositions(std::span positions) { if (positions.empty()) { @@ -426,7 +406,7 @@ namespace MWWorld mTerrainViews.emplace_back(mTerrain->createView()); } - mTerrainPreloadPositions = positions; + mTerrainPreloadPositions.assign(positions.begin(), positions.end()); if (!positions.empty()) { mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions); @@ -435,10 +415,10 @@ namespace MWWorld } } - bool CellPreloader::isTerrainLoaded(const CellPreloader::PositionCellGrid& position, double referenceTime) const + bool CellPreloader::isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const { return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime - && contains(mLoadedTerrainPositions, std::array{ position }, Constants::CellSizeInUnits); + && contains(mLoadedTerrainPositions, position, Constants::CellSizeInUnits); } void CellPreloader::setTerrain(Terrain::World* terrain) @@ -474,4 +454,12 @@ namespace MWWorld mPreloadCells.clear(); } + void CellPreloader::reportStats(unsigned int frameNumber, osg::Stats& stats) const + { + stats.setAttribute(frameNumber, "CellPreloader Count", mPreloadCells.size()); + stats.setAttribute(frameNumber, "CellPreloader Added", mAdded); + stats.setAttribute(frameNumber, "CellPreloader Evicted", mEvicted); + stats.setAttribute(frameNumber, "CellPreloader Loaded", mLoaded); + stats.setAttribute(frameNumber, "CellPreloader Expired", mExpired); + } } diff --git a/apps/openmw/mwworld/cellpreloader.hpp b/apps/openmw/mwworld/cellpreloader.hpp index ddf13cab83..405ba96a2e 100644 --- a/apps/openmw/mwworld/cellpreloader.hpp +++ b/apps/openmw/mwworld/cellpreloader.hpp @@ -1,12 +1,20 @@ #ifndef OPENMW_MWWORLD_CELLPRELOADER_H #define OPENMW_MWWORLD_CELLPRELOADER_H +#include "positioncellgrid.hpp" + #include -#include -#include -#include + #include +#include +#include + +namespace osg +{ + class Stats; +} + namespace Resource { class ResourceSystem; @@ -56,26 +64,29 @@ namespace MWWorld void setExpiryDelay(double expiryDelay); /// The minimum number of preloaded cells before unused cells get thrown out. - void setMinCacheSize(unsigned int num); + void setMinCacheSize(std::size_t value) { mMinCacheSize = value; } /// The maximum number of preloaded cells. - void setMaxCacheSize(unsigned int num); + void setMaxCacheSize(std::size_t value) { mMaxCacheSize = value; } /// Enables the creation of instances in the preloading thread. void setPreloadInstances(bool preload); - unsigned int getMaxCacheSize() const; + std::size_t getMaxCacheSize() const { return mMaxCacheSize; } + + std::size_t getCacheSize() const { return mPreloadCells.size(); } void setWorkQueue(osg::ref_ptr workQueue); - typedef std::pair PositionCellGrid; - void setTerrainPreloadPositions(const std::vector& positions); + void setTerrainPreloadPositions(std::span positions); void syncTerrainLoad(Loading::Listener& listener); void abortTerrainPreloadExcept(const PositionCellGrid* exceptPos); - bool isTerrainLoaded(const CellPreloader::PositionCellGrid& position, double referenceTime) const; + bool isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const; void setTerrain(Terrain::World* terrain); + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + private: void clearAllTasks(); @@ -85,8 +96,8 @@ namespace MWWorld MWRender::LandManager* mLandManager; osg::ref_ptr mWorkQueue; double mExpiryDelay; - unsigned int mMinCacheSize; - unsigned int mMaxCacheSize; + std::size_t mMinCacheSize = 0; + std::size_t mMaxCacheSize = 0; bool mPreloadInstances; double mLastResourceCacheUpdate; @@ -118,6 +129,10 @@ namespace MWWorld std::vector mLoadedTerrainPositions; double mLoadedTerrainTimestamp; + std::size_t mEvicted = 0; + std::size_t mAdded = 0; + std::size_t mExpired = 0; + std::size_t mLoaded = 0; }; } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 37e1be0ddf..854348c2a8 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -8,10 +8,10 @@ #include #include -#include -#include -#include -#include +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/spellutil.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" namespace MWWorld { @@ -30,17 +30,17 @@ namespace MWWorld { } - const ESM::RefNum& CellRef::getRefNum() const + ESM::RefNum CellRef::getRefNum() const noexcept { return std::visit(ESM::VisitOverload{ - [&](const ESM4::Reference& ref) -> const ESM::RefNum& { return ref.mId; }, - [&](const ESM4::ActorCharacter& ref) -> const ESM::RefNum& { return ref.mId; }, - [&](const ESM::CellRef& ref) -> const ESM::RefNum& { return ref.mRefNum; }, + [&](const ESM4::Reference& ref) -> ESM::RefNum { return ref.mId; }, + [&](const ESM4::ActorCharacter& ref) -> ESM::RefNum { return ref.mId; }, + [&](const ESM::CellRef& ref) -> ESM::RefNum { return ref.mRefNum; }, }, mCellRef.mVariant); } - const ESM::RefNum& CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) + ESM::RefNum CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) { ESM::RefNum& refNum = std::visit(ESM::VisitOverload{ [&](ESM4::Reference& ref) -> ESM::RefNum& { return ref.mId; }, @@ -188,19 +188,14 @@ namespace MWWorld void CellRef::applyChargeRemainderToBeSubtracted(float chargeRemainder) { auto esm3Visit = [&](ESM::CellRef& cellRef3) { - cellRef3.mChargeIntRemainder += std::abs(chargeRemainder); - if (cellRef3.mChargeIntRemainder > 1.0f) + cellRef3.mChargeIntRemainder -= std::abs(chargeRemainder); + if (cellRef3.mChargeIntRemainder <= -1.0f) { - float newChargeRemainder = (cellRef3.mChargeIntRemainder - std::floor(cellRef3.mChargeIntRemainder)); - if (cellRef3.mChargeInt <= static_cast(cellRef3.mChargeIntRemainder)) - { - cellRef3.mChargeInt = 0; - } - else - { - cellRef3.mChargeInt -= static_cast(cellRef3.mChargeIntRemainder); - } + float newChargeRemainder = std::modf(cellRef3.mChargeIntRemainder, &cellRef3.mChargeIntRemainder); + cellRef3.mChargeInt += static_cast(cellRef3.mChargeIntRemainder); cellRef3.mChargeIntRemainder = newChargeRemainder; + if (cellRef3.mChargeInt < 0) + cellRef3.mChargeInt = 0; } }; std::visit(ESM::VisitOverload{ @@ -211,6 +206,16 @@ namespace MWWorld mCellRef.mVariant); } + void CellRef::setChargeIntRemainder(float chargeRemainder) + { + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mChargeIntRemainder = chargeRemainder; }, + }, + mCellRef.mVariant); + } + void CellRef::setChargeFloat(float charge) { std::visit(ESM::VisitOverload{ @@ -372,17 +377,19 @@ namespace MWWorld } } - void CellRef::setGoldValue(int value) + void CellRef::setCount(int value) { - if (value != getGoldValue()) + if (value != getCount(false)) { mChanged = true; std::visit(ESM::VisitOverload{ - [&](ESM4::Reference& /*ref*/) {}, - [&](ESM4::ActorCharacter&) {}, - [&](ESM::CellRef& ref) { ref.mGoldValue = value; }, + [&](ESM4::Reference& ref) { ref.mCount = value; }, + [&](ESM4::ActorCharacter& ref) { ref.mCount = value; }, + [&](ESM::CellRef& ref) { ref.mCount = value; }, }, mCellRef.mVariant); + if (value == 0) + MWBase::Environment::get().getWorld()->removeRefScript(this); } } diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 73e721278e..4dcac4def5 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -27,11 +27,11 @@ namespace MWWorld explicit CellRef(const ESM4::ActorCharacter& ref); // Note: Currently unused for items in containers - const ESM::RefNum& getRefNum() const; + ESM::RefNum getRefNum() const noexcept; // Returns RefNum. // If RefNum is not set, assigns a generated one and changes the "lastAssignedRefNum" counter. - const ESM::RefNum& getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum); + ESM::RefNum getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum); void setRefNum(ESM::RefNum refNum); @@ -118,9 +118,22 @@ namespace MWWorld }; return std::visit(Visitor(), mCellRef.mVariant); } // Implemented as union with int charge + float getChargeIntRemainder() const + { + struct Visitor + { + float operator()(const ESM::CellRef& ref) { return ref.mChargeIntRemainder; } + float operator()(const ESM4::Reference& /*ref*/) { return 0; } + float operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } void setCharge(int charge); void setChargeFloat(float charge); - void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1 + void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if <= -1 + + // Stores fractional part of mChargeInt + void setChargeIntRemainder(float chargeRemainder); // The NPC that owns this object (and will get angry if you steal it) ESM::RefId getOwner() const @@ -218,18 +231,20 @@ namespace MWWorld } void setTrap(const ESM::RefId& trap); - // This is 5 for Gold_005 references, 100 for Gold_100 and so on. - int getGoldValue() const + int getCount(bool absolute = true) const { struct Visitor { - int operator()(const ESM::CellRef& ref) { return ref.mGoldValue; } - int operator()(const ESM4::Reference& /*ref*/) { return 0; } - int operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + int operator()(const ESM::CellRef& ref) { return ref.mCount; } + int operator()(const ESM4::Reference& ref) { return ref.mCount; } + int operator()(const ESM4::ActorCharacter& ref) { return ref.mCount; } }; - return std::visit(Visitor(), mCellRef.mVariant); + int count = std::visit(Visitor(), mCellRef.mVariant); + if (absolute) + return std::abs(count); + return count; } - void setGoldValue(int value); + void setCount(int value); // Write the content of this CellRef into the given ObjectState void writeState(ESM::ObjectState& state) const; diff --git a/apps/openmw/mwworld/cellreflist.hpp b/apps/openmw/mwworld/cellreflist.hpp index 12f086e15d..187fff4287 100644 --- a/apps/openmw/mwworld/cellreflist.hpp +++ b/apps/openmw/mwworld/cellreflist.hpp @@ -38,7 +38,7 @@ namespace MWWorld } /// Remove all references with the given refNum from this list. - void remove(const ESM::RefNum& refNum) + void remove(ESM::RefNum refNum) { for (typename List::iterator it = mList.begin(); it != mList.end();) { diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 4263be8649..4cd189bb20 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -52,12 +52,16 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include +#include #include #include #include @@ -149,7 +153,7 @@ namespace if (toIgnore.find(&*iter) != toIgnore.end()) continue; - if (actor.getClass().getCreatureStats(actor).matchesActorId(actorId) && actor.getRefData().getCount() > 0) + if (actor.getClass().getCreatureStats(actor).matchesActorId(actorId) && actor.getCellRef().getCount() > 0) return actor; } @@ -159,31 +163,32 @@ namespace template void writeReferenceCollection(ESM::ESMWriter& writer, const MWWorld::CellRefList& collection) { - if (!collection.mList.empty()) + // references + for (const MWWorld::LiveCellRef& liveCellRef : collection.mList) { - // references - for (typename MWWorld::CellRefList::List::const_iterator iter(collection.mList.begin()); - iter != collection.mList.end(); ++iter) + if (ESM::isESM4Rec(T::sRecordId)) { - if (!iter->mData.hasChanged() && !iter->mRef.hasChanged() && iter->mRef.hasContentFile()) - { - // Reference that came from a content file and has not been changed -> ignore - continue; - } - if (iter->mData.getCount() == 0 && !iter->mRef.hasContentFile()) - { - // Deleted reference that did not come from a content file -> ignore - continue; - } - using StateType = typename RecordToState::StateType; - StateType state; - iter->save(state); - - // recordId currently unused - writer.writeHNT("OBJE", collection.mList.front().mBase->sRecordId); - - state.save(writer); + // TODO: Implement loading/saving of REFR4 and ACHR4 with ESM3 reader/writer. + continue; } + if (!liveCellRef.mData.hasChanged() && !liveCellRef.mRef.hasChanged() && liveCellRef.mRef.hasContentFile()) + { + // Reference that came from a content file and has not been changed -> ignore + continue; + } + if (liveCellRef.mRef.getCount() == 0 && !liveCellRef.mRef.hasContentFile()) + { + // Deleted reference that did not come from a content file -> ignore + continue; + } + using StateType = typename RecordToState::StateType; + StateType state; + liveCellRef.save(state); + + // recordId currently unused + writer.writeHNT("OBJE", collection.mList.front().mBase->sRecordId); + + state.save(writer); } } @@ -197,8 +202,8 @@ namespace { for (auto& item : state.mInventory.mItems) { - if (item.mCount > 0 && baseItem.mItem == item.mRef.mRefID) - item.mCount = -item.mCount; + if (item.mRef.mCount > 0 && baseItem.mItem == item.mRef.mRefID) + item.mRef.mCount = -item.mRef.mCount; } } } @@ -248,19 +253,27 @@ namespace if (!record) return; - if (state.mVersion < 15) + if (state.mVersion <= ESM::MaxOldRestockingFormatVersion) fixRestocking(record, state); - if (state.mVersion < 17) + if (state.mVersion <= ESM::MaxClearModifiersFormatVersion) { if constexpr (std::is_same_v) MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory); else if constexpr (std::is_same_v) MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats); } - else if (state.mVersion < 20) + else if (state.mVersion <= ESM::MaxOldCreatureStatsFormatVersion) { if constexpr (std::is_same_v || std::is_same_v) + { MWWorld::convertStats(state.mCreatureStats); + MWWorld::convertEnchantmentSlots(state.mCreatureStats, state.mInventory); + } + } + else if (state.mVersion <= ESM::MaxActiveSpellSlotIndexFormatVersion) + { + if constexpr (std::is_same_v || std::is_same_v) + MWWorld::convertEnchantmentSlots(state.mCreatureStats, state.mInventory); } if (state.mRef.mRefNum.hasContentFile()) @@ -294,9 +307,9 @@ namespace // instance is invalid. But non-storable item are always stored in saves together with their original cell. // If a non-storable item references a content file, but is not found in this content file, // we should drop it. Likewise if this stack is empty. - if (!MWWorld::ContainerStore::isStorableType() || !state.mCount) + if (!MWWorld::ContainerStore::isStorableType() || !state.mRef.mCount) { - if (state.mCount) + if (state.mRef.mCount) Log(Debug::Warning) << "Warning: Dropping reference to " << state.mRef.mRefID << " (invalid content file link)"; return; @@ -304,9 +317,9 @@ namespace } // new reference - MWWorld::LiveCellRef ref(record); + MWWorld::LiveCellRef ref(ESM::makeBlankCellRef(), record); ref.load(state); - collection.mList.push_back(ref); + collection.mList.push_back(std::move(ref)); MWWorld::LiveCellRefBase* base = &collection.mList.back(); MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(base, cellstore)); @@ -339,6 +352,34 @@ namespace namespace MWWorld { + namespace + { + template + bool isEnabled(const T& ref, const ESMStore& store) + { + if (ref.mEsp.parent.isZeroOrUnset()) + return true; + + // Disable objects that are linked to an initially disabled parent. + // Actually when we will start working on Oblivion/Skyrim scripting we will need to: + // - use the current state of the parent instead of initial state of the parent + // - every time when the parent is enabled/disabled we should also enable/disable + // all objects that are linked to it. + // But for now we assume that the parent remains in its initial state. + if (const ESM4::Reference* parentRef = store.get().searchStatic(ref.mEsp.parent)) + { + const bool parentDisabled = parentRef->mFlags & ESM4::Rec_Disabled; + const bool inversed = ref.mEsp.flags & ESM4::EnableParent::Flag_Inversed; + if (parentDisabled != inversed) + return false; + + return isEnabled(*parentRef, store); + } + + return true; + } + } + struct CellStoreImp { CellStoreTuple mRefLists; @@ -385,9 +426,9 @@ namespace MWWorld liveCellRef.mData.setDeletedByContentFile(true); if (iter != mList.end()) - *iter = liveCellRef; + *iter = std::move(liveCellRef); else - mList.push_back(liveCellRef); + mList.push_back(std::move(liveCellRef)); } else { @@ -405,10 +446,16 @@ namespace MWWorld static void loadImpl(const R& ref, const MWWorld::ESMStore& esmStore, auto& list) { const MWWorld::Store& store = esmStore.get(); - if (const X* ptr = store.search(ref.mBaseObj)) - list.emplace_back(ref, ptr); - else + const X* ptr = store.search(ref.mBaseObj); + if (!ptr) + { Log(Debug::Warning) << "Warning: could not resolve cell reference " << ref.mId << " (dropping reference)"; + return; + } + LiveCellRef liveCellRef(ref, ptr); + if (!isEnabled(ref, esmStore)) + liveCellRef.mData.disable(); + list.push_back(std::move(liveCellRef)); } template @@ -495,11 +542,7 @@ namespace MWWorld mMovedToAnotherCell.insert(std::make_pair(object.getBase(), cellToMoveTo)); requestMergedRefsUpdate(); - MWWorld::Ptr ptr(object.getBase(), cellToMoveTo); - const Class& cls = ptr.getClass(); - if (cls.hasInventoryStore(ptr)) - cls.getInventoryStore(ptr).setActor(ptr); - return ptr; + return MWWorld::Ptr(object.getBase(), cellToMoveTo); } struct MergeVisitor @@ -560,7 +603,7 @@ namespace MWWorld return false; } - CellStore::CellStore(MWWorld::Cell cell, const MWWorld::ESMStore& esmStore, ESM::ReadersCache& readers) + CellStore::CellStore(MWWorld::Cell&& cell, const MWWorld::ESMStore& esmStore, ESM::ReadersCache& readers) : mStore(esmStore) , mReaders(readers) , mCellVariant(std::move(cell)) @@ -655,7 +698,7 @@ namespace MWWorld MWWorld::Ptr actor(base, this); if (!actor.getClass().isActor()) continue; - if (actor.getClass().getCreatureStats(actor).matchesActorId(id) && actor.getRefData().getCount() > 0) + if (actor.getClass().getCreatureStats(actor).matchesActorId(id) && actor.getCellRef().getCount() > 0) return actor; } @@ -664,10 +707,10 @@ namespace MWWorld class RefNumSearchVisitor { - const ESM::RefNum& mRefNum; + ESM::RefNum mRefNum; public: - RefNumSearchVisitor(const ESM::RefNum& refNum) + RefNumSearchVisitor(ESM::RefNum refNum) : mRefNum(refNum) { } @@ -1021,7 +1064,7 @@ namespace MWWorld for (const auto& [base, store] : mMovedToAnotherCell) { ESM::RefNum refNum = base->mRef.getRefNum(); - if (base->mData.isDeleted() && !refNum.hasContentFile()) + if (base->isDeleted() && !refNum.hasContentFile()) continue; // filtered out in writeReferenceCollection ESM::RefId movedTo = store->getCell()->getId(); @@ -1147,20 +1190,18 @@ namespace MWWorld { if (mState == State_Loaded) { - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& creature : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&creature); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& npc : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&npc); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } @@ -1175,29 +1216,26 @@ namespace MWWorld if (mState == State_Loaded) { - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& creature : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&creature); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& npc : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&npc); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (MWWorld::LiveCellRef& container : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getRefData().getCount() > 0 + Ptr ptr = getCurrentPtr(&container); + if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getCellRef().getCount() > 0 && ptr.getClass().getContainerStore(ptr).isResolved()) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); @@ -1239,13 +1277,12 @@ namespace MWWorld clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) - { - Ptr ptr = getCurrentPtr(&*it); + forEachType([](Ptr ptr) { // no need to clearCorpse, handled as part of get() - ptr.getClass().respawn(ptr); - } + if (!ptr.mRef->isDeleted()) + ptr.getClass().respawn(ptr); + return true; + }); } } @@ -1270,7 +1307,7 @@ namespace MWWorld for (auto& item : list) { Ptr ptr = getCurrentPtr(&item); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { checkItem(ptr); } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 496e98bf1a..097053e2e0 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -70,8 +70,12 @@ namespace ESM4 struct Container; struct Door; struct Furniture; + struct Flora; struct Ingredient; + struct ItemMod; struct MiscItem; + struct MovableStatic; + struct StaticCollection; struct Terminal; struct Tree; struct Weapon; @@ -93,9 +97,10 @@ namespace MWWorld CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, CellRefList, - CellRefList, CellRefList, CellRefList, - CellRefList, CellRefList, CellRefList, CellRefList, - CellRefList, CellRefList, CellRefList>; + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList>; /// \brief Mutable state of a cell class CellStore @@ -114,7 +119,7 @@ namespace MWWorld /// scripting compatibility, and the fact that objects may be "un-deleted" in the original game). static bool isAccessible(const MWWorld::RefData& refdata, const MWWorld::CellRef& cref) { - return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || refdata.getCount() > 0); + return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || cref.getCount() > 0); } /// Moves object from this cell to the given cell. @@ -140,7 +145,7 @@ namespace MWWorld } /// @param readerList The readers to use for loading of the cell on-demand. - CellStore(MWWorld::Cell cell, const MWWorld::ESMStore& store, ESM::ReadersCache& readers); + CellStore(MWWorld::Cell&& cell, const MWWorld::ESMStore& store, ESM::ReadersCache& readers); CellStore(const CellStore&) = delete; @@ -205,8 +210,8 @@ namespace MWWorld /// false will abort the iteration. /// \note Prefer using forEachConst when possible. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in - /// unintended behaviour. \attention This function also lists deleted (count 0) objects! \return Iteration - /// completed? + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? template bool forEach(Visitor&& visitor) { @@ -220,12 +225,12 @@ namespace MWWorld mHasState = true; - for (unsigned int i = 0; i < mMergedRefs.size(); ++i) + for (LiveCellRefBase* mergedRef : mMergedRefs) { - if (!isAccessible(mMergedRefs[i]->mData, mMergedRefs[i]->mRef)) + if (!isAccessible(mergedRef->mData, mergedRef->mRef)) continue; - if (!visitor(MWWorld::Ptr(mMergedRefs[i], this))) + if (!visitor(MWWorld::Ptr(mergedRef, this))) return false; } return true; @@ -234,8 +239,8 @@ namespace MWWorld /// Call visitor (MWWorld::ConstPtr) for each reference. visitor must return a bool. Returning /// false will abort the iteration. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in - /// unintended behaviour. \attention This function also lists deleted (count 0) objects! \return Iteration - /// completed? + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? template bool forEachConst(Visitor&& visitor) const { @@ -245,12 +250,12 @@ namespace MWWorld if (mMergedRefsNeedsUpdate) updateMergedRefs(); - for (unsigned int i = 0; i < mMergedRefs.size(); ++i) + for (const LiveCellRefBase* mergedRef : mMergedRefs) { - if (!isAccessible(mMergedRefs[i]->mData, mMergedRefs[i]->mRef)) + if (!isAccessible(mergedRef->mData, mergedRef->mRef)) continue; - if (!visitor(MWWorld::ConstPtr(mMergedRefs[i], this))) + if (!visitor(MWWorld::ConstPtr(mergedRef, this))) return false; } return true; @@ -259,10 +264,10 @@ namespace MWWorld /// Call visitor (ref) for each reference of given type. visitor must return a bool. Returning /// false will abort the iteration. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in - /// unintended behaviour. \attention This function also lists deleted (count 0) objects! \return Iteration - /// completed? + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? template - bool forEachType(Visitor& visitor) + bool forEachType(Visitor&& visitor) { if (mState != State_Loaded) return false; diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index b360e90471..105fbca80a 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -118,13 +119,8 @@ namespace MWWorld throw std::runtime_error("class cannot hit"); } - void Class::block(const Ptr& ptr) const - { - throw std::runtime_error("class cannot block"); - } - void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, - const osg::Vec3f& hitPosition, bool successful) const + const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } @@ -149,7 +145,7 @@ namespace MWWorld throw std::runtime_error("class does not have an inventory store"); } - bool Class::hasInventoryStore(const Ptr& ptr) const + bool Class::hasInventoryStore(const ConstPtr& ptr) const { return false; } @@ -310,19 +306,27 @@ namespace MWWorld void Class::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const {} - std::string Class::getModel(const MWWorld::ConstPtr& ptr) const + std::string_view Class::getModel(const MWWorld::ConstPtr& ptr) const { return {}; } + VFS::Path::Normalized Class::getCorrectedModel(const MWWorld::ConstPtr& ptr) const + { + std::string_view model = getModel(ptr); + if (!model.empty()) + return Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(model)); + return {}; + } + bool Class::useAnim() const { return false; } - void Class::getModelsToPreload(const Ptr& ptr, std::vector& models) const + void Class::getModelsToPreload(const ConstPtr& ptr, std::vector& models) const { - std::string model = getModel(ptr); + std::string_view model = getModel(ptr); if (!model.empty()) models.push_back(model); } @@ -373,11 +377,9 @@ namespace MWWorld { Ptr newPtr = copyToCellImpl(ptr, cell); newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference - newPtr.getRefData().setCount(count); + newPtr.getCellRef().setCount(count); newPtr.getRefData().setLuaScripts(nullptr); MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); - if (hasInventoryStore(newPtr)) - getInventoryStore(newPtr).setActor(newPtr); return newPtr; } @@ -386,8 +388,6 @@ namespace MWWorld Ptr newPtr = copyToCellImpl(ptr, cell); ptr.getRefData().setLuaScripts(nullptr); MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); - if (hasInventoryStore(newPtr)) - getInventoryStore(newPtr).setActor(newPtr); return newPtr; } @@ -528,13 +528,11 @@ namespace MWWorld const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get().search(enchantmentName); - if (!enchantment) + if (!enchantment || enchantment->mEffects.mList.empty()) return result; - assert(enchantment->mEffects.mList.size()); - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().search( - enchantment->mEffects.mList.front().mEffectID); + enchantment->mEffects.mList.front().mData.mEffectID); if (!magicEffect) return result; diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 7b7e9135ba..d3d75aa935 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -13,8 +13,11 @@ #include "ptr.hpp" #include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/damagesourcetype.hpp" + #include #include +#include namespace ESM { @@ -142,15 +145,12 @@ namespace MWWorld /// (default implementation: throw an exception) virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the - /// actor responsible for the attack, and \a successful specifies if the hit is - /// successful or not. - - virtual void block(const Ptr& ptr) const; - ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield - /// (default implementation: throw an exception) + /// actor responsible for the attack. \a successful specifies if the hit is + /// successful or not. \a sourceType classifies the damage source. virtual std::unique_ptr activate(const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). @@ -167,7 +167,7 @@ namespace MWWorld ///< Return inventory store or throw an exception, if class does not have a /// inventory store (default implementation: throw an exception) - virtual bool hasInventoryStore(const Ptr& ptr) const; + virtual bool hasInventoryStore(const ConstPtr& ptr) const; ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) virtual bool canLock(const ConstPtr& ptr) const; @@ -276,12 +276,14 @@ namespace MWWorld virtual int getServices(const MWWorld::ConstPtr& actor) const; - virtual std::string getModel(const MWWorld::ConstPtr& ptr) const; + virtual std::string_view getModel(const MWWorld::ConstPtr& ptr) const; + + virtual VFS::Path::Normalized getCorrectedModel(const MWWorld::ConstPtr& ptr) const; virtual bool useAnim() const; ///< Whether or not to use animated variant of model (default false) - virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const; + virtual void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: ///< list getModel(). diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index f5e45415ae..5a7c5b5333 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -51,7 +51,7 @@ namespace for (const MWWorld::LiveCellRef& liveCellRef : cellRefList.mList) { - if (const int count = liveCellRef.mData.getCount(); count > 0) + if (const int count = liveCellRef.mRef.getCount(); count > 0) sum += count * liveCellRef.mBase->mData.mWeight; } @@ -65,7 +65,7 @@ namespace for (MWWorld::LiveCellRef& liveCellRef : list.mList) { - if ((liveCellRef.mBase->mId == id) && liveCellRef.mData.getCount()) + if ((liveCellRef.mBase->mId == id) && liveCellRef.mRef.getCount()) { MWWorld::Ptr ptr(&liveCellRef, nullptr); ptr.setContainerStore(store); @@ -101,9 +101,9 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState( if (!record) return ContainerStoreIterator(this); - LiveCellRef ref(record); + LiveCellRef ref(ESM::makeBlankCellRef(), record); ref.load(state); - collection.mList.push_back(ref); + collection.mList.push_back(std::move(ref)); auto it = ContainerStoreIterator(this, --collection.mList.end()); MWBase::Environment::get().getWorldModel()->registerPtr(*it); @@ -111,12 +111,12 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState( } void MWWorld::ContainerStore::storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { } void MWWorld::ContainerStore::readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { } @@ -128,11 +128,11 @@ void MWWorld::ContainerStore::storeState(const LiveCellRef& ref, ESM::ObjectS template void MWWorld::ContainerStore::storeStates( - const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable) const + const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, bool equipable) const { for (const LiveCellRef& liveCellRef : collection.mList) { - if (liveCellRef.mData.getCount() == 0) + if (liveCellRef.mRef.getCount() == 0) continue; ESM::ObjectState state; storeState(liveCellRef, state); @@ -157,13 +157,6 @@ MWWorld::ContainerStore::ContainerStore() { } -MWWorld::ContainerStore::~ContainerStore() -{ - MWWorld::WorldModel* worldModel = MWBase::Environment::get().getWorldModel(); - for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) - worldModel->deregisterPtr(*iter); -} - MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cbegin(int mask) const { return ConstContainerStoreIterator(mask, this); @@ -199,7 +192,7 @@ int MWWorld::ContainerStore::count(const ESM::RefId& id) const int total = 0; for (const auto&& iter : *this) if (iter.getCellRef().getRefId() == id) - total += iter.getRefData().getCount(); + total += iter.getCellRef().getCount(); return total; } @@ -226,9 +219,9 @@ void MWWorld::ContainerStore::setContListener(MWWorld::ContainerStoreListener* l MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr& ptr, int count) { resolve(); - if (ptr.getRefData().getCount() <= count) + if (ptr.getCellRef().getCount() <= count) return end(); - MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getRefData().getCount(false), count)); + MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getCellRef().getCount(false), count)); MWWorld::Ptr newPtr = *it; newPtr.getCellRef().unsetRefNum(); @@ -239,7 +232,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr& ptr, if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, *it); - remove(ptr, ptr.getRefData().getCount() - count); + remove(ptr, ptr.getCellRef().getCount() - count); return it; } @@ -264,9 +257,9 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld:: { if (stacks(*iter, item)) { - iter->getRefData().setCount( - addItems(iter->getRefData().getCount(false), item.getRefData().getCount(false))); - item.getRefData().setCount(0); + iter->getCellRef().setCount( + addItems(iter->getCellRef().getCount(false), item.getCellRef().getCount(false))); + item.getCellRef().setCount(0); retval = iter; break; } @@ -350,7 +343,8 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add( const ESM::RefId& script = item.getClass().getScript(item); if (!script.empty()) { - if (mActor == player) + const Ptr& contPtr = getPtr(); + if (contPtr == player) { // Items in player's inventory have cell set to 0, so their scripts will never be removed item.mCell = nullptr; @@ -359,10 +353,8 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add( { // Set mCell to the cell of the container/actor, so that the scripts are removed properly when // the cell of the container/actor goes inactive - if (!mPtr.isEmpty()) - item.mCell = mPtr.getCell(); - else if (!mActor.isEmpty()) - item.mCell = mActor.getCell(); + if (!contPtr.isEmpty()) + item.mCell = contPtr.getCell(); } item.mContainerStore = this; @@ -371,12 +363,12 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add( // Set OnPCAdd special variable, if it is declared // Make sure to do this *after* we have added the script to LocalScripts - if (mActor == player) + if (contPtr == player) item.getRefData().getLocals().setVarByInt(script, "onpcadd", 1); } // we should not fire event for InventoryStore yet - it has some custom logic - if (mListener && !(!mActor.isEmpty() && mActor.getClass().hasInventoryStore(mActor))) + if (mListener && typeid(*this) == typeid(ContainerStore)) mListener->itemAdded(item, count); return it; @@ -393,36 +385,38 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp(const Ptr& ptr, // gold needs special handling: when it is inserted into a container, the base object automatically becomes Gold_001 // this ensures that gold piles of different sizes stack with each other (also, several scripts rely on Gold_001 for // detecting player gold) + // Note that adding 1 gold_100 is equivalent to adding 1 gold_001. Morrowind.exe resolves gold in leveled lists to + // gold_001 and TESCS disallows adding gold other than gold_001 to inventories. If a content file defines a + // container containing gold_100 anyway, the item is not turned to gold_001 until the player puts it down in the + // world and picks it up again. We just turn it into gold_001 here and ignore that oddity. if (ptr.getClass().isGold(ptr)) { - int realCount = count * ptr.getClass().getValue(ptr); - for (MWWorld::ContainerStoreIterator iter(begin(type)); iter != end(); ++iter) { if (iter->getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), realCount)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; } } - MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, realCount); - return addNewStack(ref.getPtr(), realCount); + MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, count); + return addNewStack(ref.getPtr(), count); } // determine whether to stack or not for (MWWorld::ContainerStoreIterator iter(begin(type)); iter != end(); ++iter) { // Don't stack with equipped items - if (!mActor.isEmpty() && mActor.getClass().hasInventoryStore(mActor)) - if (mActor.getClass().getInventoryStore(mActor).isEquipped(*iter)) + if (auto* inventoryStore = dynamic_cast(this)) + if (inventoryStore->isEquipped(*iter)) continue; if (stacks(*iter, ptr)) { // stack - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; @@ -488,7 +482,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addNewStack(const Const break; } - it->getRefData().setCount(count); + it->getCellRef().setCount(count); flagAsModified(); return it; @@ -570,7 +564,7 @@ int MWWorld::ContainerStore::remove(const Ptr& item, int count, bool equipReplac resolve(); int toRemove = count; - RefData& itemRef = item.getRefData(); + CellRef& itemRef = item.getCellRef(); if (itemRef.getCount() <= toRemove) { @@ -586,7 +580,7 @@ int MWWorld::ContainerStore::remove(const Ptr& item, int count, bool equipReplac flagAsModified(); // we should not fire event for InventoryStore yet - it has some custom logic - if (mListener && !(!mActor.isEmpty() && mActor.getClass().hasInventoryStore(mActor))) + if (mListener && typeid(*this) == typeid(ContainerStore)) mListener->itemRemoved(item, count - toRemove); // number of removed items @@ -675,7 +669,7 @@ void MWWorld::ContainerStore::addInitialItemImp( void MWWorld::ContainerStore::clear() { for (auto&& iter : *this) - iter.getRefData().setCount(0); + iter.getCellRef().setCount(0); flagAsModified(); mModified = true; @@ -694,13 +688,14 @@ bool MWWorld::ContainerStore::isResolved() const void MWWorld::ContainerStore::resolve() { - if (!mResolved && !mPtr.isEmpty()) + const Ptr& container = getPtr(); + if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); Misc::Rng::Generator prng{ mSeed }; - fill(mPtr.get()->mBase->mInventory, ESM::RefId(), prng); - addScripts(*this, mPtr.mCell); + fill(container.get()->mBase->mInventory, ESM::RefId(), prng); + addScripts(*this, container.mCell); } mModified = true; } @@ -715,15 +710,16 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() listener = std::make_shared(*this); mResolutionListener = listener; } - if (!mResolved && !mPtr.isEmpty()) + const Ptr& container = getPtr(); + if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) - ptr.getRefData().setCount(0); + ptr.getCellRef().setCount(0); Misc::Rng::Generator prng{ mSeed }; - fill(mPtr.get()->mBase->mInventory, ESM::RefId(), prng); - addScripts(*this, mPtr.mCell); + fill(container.get()->mBase->mInventory, ESM::RefId(), prng); + addScripts(*this, container.mCell); } - return { listener }; + return { std::move(listener) }; } void MWWorld::ContainerStore::unresolve() @@ -731,12 +727,13 @@ void MWWorld::ContainerStore::unresolve() if (mModified) return; - if (mResolved && !mPtr.isEmpty()) + const Ptr& container = getPtr(); + if (mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) - ptr.getRefData().setCount(0); - fillNonRandom(mPtr.get()->mBase->mInventory, ESM::RefId(), mSeed); - addScripts(*this, mPtr.mCell); + ptr.getCellRef().setCount(0); + fillNonRandom(container.get()->mBase->mInventory, ESM::RefId(), mSeed); + addScripts(*this, container.mCell); mResolved = false; } } @@ -931,7 +928,7 @@ void MWWorld::ContainerStore::writeState(ESM::InventoryState& state) const { state.mItems.clear(); - int index = 0; + size_t index = 0; storeStates(potions, state, index); storeStates(appas, state, index); storeStates(armors, state, index, true); @@ -952,12 +949,12 @@ void MWWorld::ContainerStore::readState(const ESM::InventoryState& inventory) mModified = true; mResolved = true; - int index = 0; + size_t index = 0; for (const ESM::ObjectState& state : inventory.mItems) { int type = MWBase::Environment::get().getESMStore()->find(state.mRef.mRefID); - int thisIndex = index++; + size_t thisIndex = index++; switch (type) { @@ -1337,7 +1334,7 @@ MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBas { if (incIterator()) nextType(); - } while (mType != -1 && !(**this).getRefData().getCount()); + } while (mType != -1 && !(**this).getCellRef().getCount()); return *this; } @@ -1389,7 +1386,7 @@ MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase(int mas { nextType(); - if (mType == -1 || (**this).getRefData().getCount()) + if (mType == -1 || (**this).getCellRef().getCount()) return; ++*this; diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index bced5820fa..fb2722dde8 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -125,11 +125,6 @@ namespace MWWorld bool mRechargingItemsUpToDate; - // Non-empty only if is InventoryStore. - // The actor whose inventory it is. - // TODO: Consider merging mActor and mPtr. - MWWorld::Ptr mActor; - private: MWWorld::CellRefList potions; MWWorld::CellRefList appas; @@ -150,7 +145,7 @@ namespace MWWorld bool mModified; bool mResolved; unsigned int mSeed; - MWWorld::Ptr mPtr; // Container that contains this store. Set in MWClass::Container::getContainerStore + MWWorld::SafePtr mPtr; // Container or actor that holds this store. std::weak_ptr mResolutionListener; ContainerStoreIterator addImp(const Ptr& ptr, int count, bool markModified = true); @@ -166,21 +161,21 @@ namespace MWWorld void storeState(const LiveCellRef& ref, ESM::ObjectState& state) const; template - void storeStates( - const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable = false) const; + void storeStates(const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, + bool equipable = false) const; void updateRechargingItems(); virtual void storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const; + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const; virtual void readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory); + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory); public: ContainerStore(); - virtual ~ContainerStore(); + virtual ~ContainerStore() = default; virtual std::unique_ptr clone() { @@ -189,6 +184,10 @@ namespace MWWorld return res; } + // Container or actor that holds this store. + const Ptr& getPtr() const { return mPtr.ptrOrEmpty(); } + void setPtr(const Ptr& ptr) { mPtr = SafePtr(ptr); } + ConstContainerStoreIterator cbegin(int mask = Type_All) const; ConstContainerStoreIterator cend() const; ConstContainerStoreIterator begin(int mask = Type_All) const; diff --git a/apps/openmw/mwworld/customdata.cpp b/apps/openmw/mwworld/customdata.cpp index 395d230a1a..db340302ce 100644 --- a/apps/openmw/mwworld/customdata.cpp +++ b/apps/openmw/mwworld/customdata.cpp @@ -77,4 +77,17 @@ namespace MWWorld throw std::logic_error(error.str()); } + MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData"; + throw std::logic_error(error.str()); + } + + const MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData"; + throw std::logic_error(error.str()); + } } diff --git a/apps/openmw/mwworld/customdata.hpp b/apps/openmw/mwworld/customdata.hpp index 9d9283f085..8051876309 100644 --- a/apps/openmw/mwworld/customdata.hpp +++ b/apps/openmw/mwworld/customdata.hpp @@ -6,6 +6,7 @@ namespace MWClass { class CreatureCustomData; + class ESM4NpcCustomData; class NpcCustomData; class ContainerCustomData; class DoorCustomData; @@ -38,6 +39,9 @@ namespace MWWorld virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; + + virtual MWClass::ESM4NpcCustomData& asESM4NpcCustomData(); + virtual const MWClass::ESM4NpcCustomData& asESM4NpcCustomData() const; }; template diff --git a/apps/openmw/mwworld/datetimemanager.cpp b/apps/openmw/mwworld/datetimemanager.cpp index 7559242bef..633aeb42f0 100644 --- a/apps/openmw/mwworld/datetimemanager.cpp +++ b/apps/openmw/mwworld/datetimemanager.cpp @@ -4,6 +4,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -182,22 +183,19 @@ namespace MWWorld return months[month]; } - bool DateTimeManager::updateGlobalFloat(GlobalVariableName name, float value) + void DateTimeManager::updateGlobalFloat(GlobalVariableName name, float value) { if (name == Globals::sGameHour) { setHour(value); - return true; } else if (name == Globals::sDay) { setDay(static_cast(value)); - return true; } else if (name == Globals::sMonth) { setMonth(static_cast(value)); - return true; } else if (name == Globals::sYear) { @@ -211,26 +209,21 @@ namespace MWWorld { mDaysPassed = static_cast(value); } - - return false; } - bool DateTimeManager::updateGlobalInt(GlobalVariableName name, int value) + void DateTimeManager::updateGlobalInt(GlobalVariableName name, int value) { if (name == Globals::sGameHour) { setHour(static_cast(value)); - return true; } else if (name == Globals::sDay) { setDay(value); - return true; } else if (name == Globals::sMonth) { setMonth(value); - return true; } else if (name == Globals::sYear) { @@ -244,8 +237,6 @@ namespace MWWorld { mDaysPassed = value; } - - return false; } void DateTimeManager::setSimulationTimeScale(float scale) @@ -263,6 +254,9 @@ namespace MWWorld void DateTimeManager::updateIsPaused() { - mPaused = !mPausedTags.empty() || MWBase::Environment::get().getWindowManager()->isGuiMode(); + auto stateManager = MWBase::Environment::get().getStateManager(); + auto wm = MWBase::Environment::get().getWindowManager(); + mPaused = !mPausedTags.empty() || wm->isConsoleMode() || wm->isPostProcessorHudVisible() + || wm->isInteractiveMessageBoxActive() || stateManager->getState() == MWBase::StateManager::State_NoGame; } } diff --git a/apps/openmw/mwworld/datetimemanager.hpp b/apps/openmw/mwworld/datetimemanager.hpp index f89894292f..fce8898cf2 100644 --- a/apps/openmw/mwworld/datetimemanager.hpp +++ b/apps/openmw/mwworld/datetimemanager.hpp @@ -29,7 +29,11 @@ namespace MWWorld float getGameTimeScale() const { return mGameTimeScale; } void setGameTimeScale(float scale); // game time to simulation time ratio - // Simulation time (the number of seconds passed from the beginning of the game). + // Rendering simulation time (summary simulation time of rendering frames since application start). + double getRenderingSimulationTime() const { return mRenderingSimulationTime; } + void setRenderingSimulationTime(double t) { mRenderingSimulationTime = t; } + + // World simulation time (the number of seconds passed from the beginning of the game). double getSimulationTime() const { return mSimulationTime; } void setSimulationTime(double t) { mSimulationTime = t; } float getSimulationTimeScale() const { return mSimulationTimeScale; } @@ -49,8 +53,8 @@ namespace MWWorld private: friend class World; void setup(Globals& globalVariables); - bool updateGlobalInt(GlobalVariableName name, int value); - bool updateGlobalFloat(GlobalVariableName name, float value); + void updateGlobalInt(GlobalVariableName name, int value); + void updateGlobalFloat(GlobalVariableName name, float value); void advanceTime(double hours, Globals& globalVariables); void setHour(double hour); @@ -64,6 +68,7 @@ namespace MWWorld float mGameHour = 0.f; float mGameTimeScale = 0.f; float mSimulationTimeScale = 1.0; + double mRenderingSimulationTime = 0.0; double mSimulationTime = 0.0; bool mPaused = false; std::set> mPausedTags; diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index e586a4c204..0be90c65f0 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -64,11 +64,12 @@ namespace MWWorld } case ESM::Format::Tes4: { - ESM4::Reader readerESM4(std::move(stream), filepath, - MWBase::Environment::get().getResourceSystem()->getVFS(), mReaders.getStatelessEncoder()); - readerESM4.setModIndex(index); - readerESM4.updateModIndices(mNameToIndex); - mStore.loadESM4(readerESM4); + ESM4::Reader reader(std::move(stream), filepath, + MWBase::Environment::get().getResourceSystem()->getVFS(), + mEncoder != nullptr ? &mEncoder->getStatelessEncoder() : nullptr); + reader.setModIndex(index); + reader.updateModIndices(mNameToIndex); + mStore.loadESM4(reader); break; } } diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 209fc39b42..15a687f4d7 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -83,11 +83,22 @@ namespace throw std::runtime_error("List of NPC classes is empty!"); } + const ESM::RefId& getDefaultRace(const MWWorld::Store& races) + { + auto it = races.begin(); + if (it != races.end()) + return it->mId; + throw std::runtime_error("List of NPC races is empty!"); + } + std::vector getNPCsToReplace(const MWWorld::Store& factions, - const MWWorld::Store& classes, const std::unordered_map& npcs) + const MWWorld::Store& classes, const MWWorld::Store& races, + const MWWorld::Store& scripts, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found const ESM::RefId& defaultCls = getDefaultClass(classes); + // Same for races + const ESM::RefId& defaultRace = getDefaultRace(races); // Validate NPCs for non-existing class and faction. // We will replace invalid entries by fixed ones @@ -112,8 +123,7 @@ namespace } } - const ESM::RefId& npcClass = npc.mClass; - const ESM::Class* cls = classes.search(npcClass); + const ESM::Class* cls = classes.search(npc.mClass); if (!cls) { Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent class " @@ -122,6 +132,23 @@ namespace changed = true; } + const ESM::Race* race = races.search(npc.mRace); + if (!race) + { + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent race " << npc.mRace + << ", using " << defaultRace << " race as replacement."; + npc.mRace = defaultRace; + changed = true; + } + + if (!npc.mScript.empty() && !scripts.search(npc.mScript)) + { + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent script " + << npc.mScript << ", ignoring it."; + npc.mScript = ESM::RefId(); + changed = true; + } + if (changed) npcsToReplace.push_back(npc); } @@ -129,6 +156,59 @@ namespace return npcsToReplace; } + template + std::vector getSpellsToReplace( + const MWWorld::Store& spells, const MWWorld::Store& magicEffects) + { + std::vector spellsToReplace; + + for (RecordType spell : spells) + { + if (spell.mEffects.mList.empty()) + continue; + + bool changed = false; + auto iter = spell.mEffects.mList.begin(); + while (iter != spell.mEffects.mList.end()) + { + const ESM::MagicEffect* mgef = magicEffects.search(iter->mData.mEffectID); + if (!mgef) + { + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping invalid effect (index " << iter->mData.mEffectID << ")"; + iter = spell.mEffects.mList.erase(iter); + changed = true; + continue; + } + + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mData.mAttribute != -1) + { + iter->mData.mAttribute = -1; + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping unexpected attribute argument of " + << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect"; + changed = true; + } + + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mData.mSkill != -1) + { + iter->mData.mSkill = -1; + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping unexpected skill argument of " + << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect"; + changed = true; + } + + ++iter; + } + + if (changed) + spellsToReplace.emplace_back(spell); + } + + return spellsToReplace; + } + // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no // longer exists however. So instead of removing the item altogether, we're only removing the script. template @@ -138,9 +218,9 @@ namespace { if (!item.mScript.empty() && !scripts.search(item.mScript)) { + Log(Debug::Verbose) << MapT::mapped_type::getRecordType() << ' ' << id << " (" << item.mName + << ") has nonexistent script " << item.mScript << ", ignoring it."; item.mScript = ESM::RefId(); - Log(Debug::Verbose) << "Item " << id << " (" << item.mName << ") has nonexistent script " - << item.mScript << ", ignoring it."; } } } @@ -284,14 +364,18 @@ namespace MWWorld case ESM::REC_CONT4: case ESM::REC_CREA4: case ESM::REC_DOOR4: + case ESM::REC_FLOR4: case ESM::REC_FURN4: + case ESM::REC_IMOD4: case ESM::REC_INGR4: case ESM::REC_LIGH4: case ESM::REC_LVLI4: case ESM::REC_LVLC4: case ESM::REC_LVLN4: case ESM::REC_MISC4: + case ESM::REC_MSTT4: case ESM::REC_NPC_4: + case ESM::REC_SCOL4: case ESM::REC_STAT4: case ESM::REC_TERM4: case ESM::REC_TREE4: @@ -307,11 +391,6 @@ namespace MWWorld if (listener != nullptr) listener->setProgressRange(::EsmLoader::fileProgress); - // Land texture loading needs to use a separate internal store for each plugin. - // We set the number of plugins here so we can properly verify if valid plugin - // indices are being passed to the LandTexture Store retrieval methods. - getWritable().resize(esm.getIndex() + 1); - // Loop through all records while (esm.hasMoreRecs()) { @@ -516,8 +595,8 @@ namespace MWWorld void ESMStore::validate() { auto& npcs = getWritable(); - std::vector npcsToReplace - = getNPCsToReplace(getWritable(), getWritable(), npcs.mStatic); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mStatic); for (const ESM::NPC& npc : npcsToReplace) { @@ -525,71 +604,26 @@ namespace MWWorld npcs.insertStatic(npc); } - // Validate spell effects for invalid arguments - std::vector spellsToReplace; + removeMissingScripts(getWritable(), getWritable().mStatic); + + // Validate spell effects and enchantments for invalid arguments auto& spells = getWritable(); - for (ESM::Spell spell : spells) - { - if (spell.mEffects.mList.empty()) - continue; - - bool changed = false; - auto iter = spell.mEffects.mList.begin(); - while (iter != spell.mEffects.mList.end()) - { - const ESM::MagicEffect* mgef = getWritable().search(iter->mEffectID); - if (!mgef) - { - Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index " - << iter->mEffectID << ") present. Dropping the effect."; - iter = spell.mEffects.mList.erase(iter); - changed = true; - continue; - } - - if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) - { - if (iter->mAttribute != -1) - { - iter->mAttribute = -1; - Log(Debug::Verbose) - << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" << spell.mId - << "' has an attribute argument present. Dropping the argument."; - changed = true; - } - } - else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) - { - if (iter->mSkill != -1) - { - iter->mSkill = -1; - Log(Debug::Verbose) - << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" << spell.mId - << "' has a skill argument present. Dropping the argument."; - changed = true; - } - } - else if (iter->mSkill != -1 || iter->mAttribute != -1) - { - iter->mSkill = -1; - iter->mAttribute = -1; - Log(Debug::Verbose) << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" - << spell.mId << "' has argument(s) present. Dropping the argument(s)."; - changed = true; - } - - ++iter; - } - - if (changed) - spellsToReplace.emplace_back(spell); - } + auto& enchantments = getWritable(); + auto& magicEffects = getWritable(); + std::vector spellsToReplace = getSpellsToReplace(spells, magicEffects); for (const ESM::Spell& spell : spellsToReplace) { spells.eraseStatic(spell.mId); spells.insertStatic(spell); } + + std::vector enchantmentsToReplace = getSpellsToReplace(enchantments, magicEffects); + for (const ESM::Enchantment& enchantment : enchantmentsToReplace) + { + enchantments.eraseStatic(enchantment.mId); + enchantments.insertStatic(enchantment); + } } void ESMStore::movePlayerRecord() @@ -604,8 +638,8 @@ namespace MWWorld auto& npcs = getWritable(); auto& scripts = getWritable(); - std::vector npcsToReplace - = getNPCsToReplace(getWritable(), getWritable(), npcs.mDynamic); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mDynamic); for (const ESM::NPC& npc : npcsToReplace) npcs.insert(npc); @@ -613,6 +647,7 @@ namespace MWWorld removeMissingScripts(scripts, getWritable().mDynamic); removeMissingScripts(scripts, getWritable().mDynamic); removeMissingScripts(scripts, getWritable().mDynamic); + removeMissingScripts(scripts, getWritable().mDynamic); removeMissingScripts(scripts, getWritable().mDynamic); removeMissingObjects(getWritable()); @@ -649,7 +684,7 @@ namespace MWWorld + get().getDynamicSize() + get().getDynamicSize() + get().getDynamicSize() + get().getDynamicSize() + get().getDynamicSize() + get().getDynamicSize() - + get().getDynamicSize(); + + get().getDynamicSize() + get().getDynamicSize(); } void ESMStore::write(ESM::ESMWriter& writer, Loading::Listener& progress) const @@ -675,6 +710,7 @@ namespace MWWorld get().write(writer, progress); get().write(writer, progress); get().write(writer, progress); + get().write(writer, progress); } bool ESMStore::readRecord(ESM::ESMReader& reader, uint32_t type_id) @@ -694,6 +730,7 @@ namespace MWWorld case ESM::REC_WEAP: case ESM::REC_LEVI: case ESM::REC_LEVC: + case ESM::REC_LIGH: mStoreImp->mRecNameToStore[type]->read(reader); return true; case ESM::REC_NPC_: @@ -704,7 +741,16 @@ namespace MWWorld case ESM::REC_DYNA: reader.getSubNameIs("COUN"); - reader.getHT(mDynamicCount); + if (reader.getFormatVersion() <= ESM::MaxActiveSpellTypeVersion) + { + uint32_t dynamicCount32 = 0; + reader.getHT(dynamicCount32); + mDynamicCount = dynamicCount32; + } + else + { + reader.getHT(mDynamicCount); + } return true; default: diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 80cd5719e2..d8cfd1dcdf 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -86,22 +86,27 @@ namespace ESM4 struct Container; struct Creature; struct Door; + struct Flora; struct Furniture; struct Hair; struct HeadPart; struct Ingredient; + struct ItemMod; struct Land; + struct LandTexture; struct LevelledCreature; struct LevelledItem; struct LevelledNpc; struct Light; struct MiscItem; + struct MovableStatic; struct Npc; struct Outfit; struct Potion; struct Race; struct Reference; struct Static; + struct StaticCollection; struct Terminal; struct Tree; struct Weapon; @@ -136,11 +141,13 @@ namespace MWWorld Store, Store, Store, Store, Store, Store, Store, Store, Store, - Store, Store, Store, Store, Store, - Store, Store, Store, Store, - Store, Store, Store, Store, + Store, Store, Store, Store, + Store, Store, Store, Store, Store, + Store, Store, Store, Store, + Store, Store, Store, Store, Store, Store, Store, Store, Store, - Store, Store, Store, Store, Store>; + Store, Store, Store, Store, + Store, Store>; private: template @@ -157,7 +164,7 @@ namespace MWWorld std::vector mStores; std::vector mDynamicStores; - unsigned int mDynamicCount; + uint64_t mDynamicCount; mutable std::unordered_map> mSpellListCache; @@ -204,6 +211,7 @@ namespace MWWorld void clearDynamic(); void rebuildIdsIndex(); + ESM::RefId generateId() { return ESM::RefId::generated(mDynamicCount++); } void movePlayerRecord(); @@ -224,7 +232,7 @@ namespace MWWorld template const T* insert(const T& x) { - const ESM::RefId id = ESM::RefId::generated(mDynamicCount++); + const ESM::RefId id = generateId(); Store& store = getWritable(); if (store.search(id) != nullptr) diff --git a/apps/openmw/mwworld/globals.cpp b/apps/openmw/mwworld/globals.cpp index 264109d17f..4977df56c0 100644 --- a/apps/openmw/mwworld/globals.cpp +++ b/apps/openmw/mwworld/globals.cpp @@ -99,7 +99,7 @@ namespace MWWorld global.load(reader, isDeleted); if (const auto iter = mVariables.find(global.mId); iter != mVariables.end()) - iter->second = global; + iter->second = std::move(global); return true; } diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp index 85b9376f0d..ea5b5baf5e 100644 --- a/apps/openmw/mwworld/groundcoverstore.cpp +++ b/apps/openmw/mwworld/groundcoverstore.cpp @@ -9,8 +9,6 @@ #include #include -#include - #include "store.hpp" namespace MWWorld @@ -23,31 +21,27 @@ namespace MWWorld query.mLoadCells = true; ESM::ReadersCache readers; - const ::EsmLoader::EsmData content + ::EsmLoader::EsmData content = ::EsmLoader::loadEsmData(query, groundcoverFiles, fileCollections, readers, encoder, listener); - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - - static constexpr std::string_view prefix = "grass\\"; + static constexpr std::string_view prefix = "grass/"; for (const ESM::Static& stat : statics) { - std::string model = Misc::StringUtils::lowerCase(stat.mModel); - std::replace(model.begin(), model.end(), '/', '\\'); - if (!model.starts_with(prefix)) + VFS::Path::Normalized model = VFS::Path::toNormalized(stat.mModel); + if (!model.value().starts_with(prefix)) continue; - mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model, vfs); + mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); } for (const ESM::Static& stat : content.mStatics) { - std::string model = Misc::StringUtils::lowerCase(stat.mModel); - std::replace(model.begin(), model.end(), '/', '\\'); - if (!model.starts_with(prefix)) + VFS::Path::Normalized model = VFS::Path::toNormalized(stat.mModel); + if (!model.value().starts_with(prefix)) continue; - mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model, vfs); + mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); } - for (const ESM::Cell& cell : content.mCells) + for (ESM::Cell& cell : content.mCells) { if (!cell.isExterior()) continue; @@ -56,15 +50,6 @@ namespace MWWorld } } - std::string GroundcoverStore::getGroundcoverModel(const ESM::RefId& id) const - { - auto search = mMeshCache.find(id); - if (search == mMeshCache.end()) - return std::string(); - - return search->second; - } - void GroundcoverStore::initCell(ESM::Cell& cell, int cellX, int cellY) const { cell.blank(); diff --git a/apps/openmw/mwworld/groundcoverstore.hpp b/apps/openmw/mwworld/groundcoverstore.hpp index 2f34aa9675..021bdb5e91 100644 --- a/apps/openmw/mwworld/groundcoverstore.hpp +++ b/apps/openmw/mwworld/groundcoverstore.hpp @@ -2,6 +2,8 @@ #define GAME_MWWORLD_GROUNDCOVER_STORE_H #include +#include + #include #include #include @@ -36,7 +38,7 @@ namespace MWWorld class GroundcoverStore { private: - std::map mMeshCache; + std::map mMeshCache; std::map, std::vector> mCellContexts; public: @@ -44,7 +46,14 @@ namespace MWWorld const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener); - std::string getGroundcoverModel(const ESM::RefId& id) const; + VFS::Path::NormalizedView getGroundcoverModel(ESM::RefId id) const + { + auto it = mMeshCache.find(id); + if (it == mMeshCache.end()) + return {}; + return it->second; + } + void initCell(ESM::Cell& cell, int cellX, int cellY) const; }; } diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 99949a73d4..f48f4e6e31 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -46,32 +46,32 @@ void MWWorld::InventoryStore::initSlots(TSlots& slots_) } void MWWorld::InventoryStore::storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { - for (int i = 0; i < static_cast(mSlots.size()); ++i) + for (int32_t i = 0; i < MWWorld::InventoryStore::Slots; ++i) + { if (mSlots[i].getType() != -1 && mSlots[i]->getBase() == &ref) - { - inventory.mEquipmentSlots[index] = i; - } + inventory.mEquipmentSlots[static_cast(index)] = i; + } if (mSelectedEnchantItem.getType() != -1 && mSelectedEnchantItem->getBase() == &ref) inventory.mSelectedEnchantItem = index; } void MWWorld::InventoryStore::readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { if (index == inventory.mSelectedEnchantItem) mSelectedEnchantItem = iter; - std::map::const_iterator found = inventory.mEquipmentSlots.find(index); + auto found = inventory.mEquipmentSlots.find(index); if (found != inventory.mEquipmentSlots.end()) { if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots) throw std::runtime_error("Invalid slot index in inventory state"); // make sure the item can actually be equipped in this slot - int slot = found->second; + int32_t slot = found->second; std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); if (!allowedSlots.first.size()) return; @@ -79,11 +79,11 @@ void MWWorld::InventoryStore::readEquipmentState( slot = allowedSlots.first.front(); // unstack if required - if (!allowedSlots.second && iter->getRefData().getCount() > 1) + if (!allowedSlots.second && iter->getCellRef().getCount() > 1) { - int count = iter->getRefData().getCount(false); + int count = iter->getCellRef().getCount(false); MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, count > 0 ? 1 : -1); - iter->getRefData().setCount(subtractItems(count, 1)); + iter->getCellRef().setCount(subtractItems(count, 1)); mSlots[slot] = newIter; } else @@ -133,8 +133,9 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add( = MWWorld::ContainerStore::add(itemPtr, count, allowAutoEquip, resolve); // Auto-equip items if an armor/clothing item is added, but not for the player nor werewolves - if (allowAutoEquip && mActor != MWMechanics::getPlayer() && mActor.getClass().isNpc() - && !mActor.getClass().getNpcStats(mActor).isWerewolf()) + const Ptr& actor = getPtr(); + if (allowAutoEquip && actor != MWMechanics::getPlayer() && actor.getClass().isNpc() + && !actor.getClass().getNpcStats(actor).isWerewolf()) { auto type = itemPtr.getType(); if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) @@ -170,7 +171,7 @@ void MWWorld::InventoryStore::equip(int slot, const ContainerStoreIterator& iter // unstack item pointed to by iterator if required if (iterator != end() && !slots_.second - && iterator->getRefData().getCount() > 1) // if slots.second is true, item can stay stacked when equipped + && iterator->getCellRef().getCount() > 1) // if slots.second is true, item can stay stacked when equipped { unstack(*iterator); } @@ -220,12 +221,13 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot(int slot) cons void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) { - if (!mActor.getClass().isNpc()) + const Ptr& actor = getPtr(); + if (!actor.getClass().isNpc()) { // In original game creatures do not autoequip weapon, but we need it for weapon sheathing. // The only case when the difference is noticable - when this creature sells weapon. // So just disable weapon autoequipping for creatures which sells weapon. - int services = mActor.getClass().getServices(mActor); + int services = actor.getClass().getServices(actor); bool sellsWeapon = services & (ESM::NPC::Weapon | ESM::NPC::MagicItems); if (sellsWeapon) return; @@ -280,7 +282,7 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) for (int j = 0; j < static_cast(weaponSkillsLength); ++j) { - float skillValue = mActor.getClass().getSkill(mActor, weaponSkills[j]); + float skillValue = actor.getClass().getSkill(actor, weaponSkills[j]); if (skillValue > max && !weaponSkillVisited[j]) { max = skillValue; @@ -323,7 +325,7 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) } } - if (weapon != end() && weapon->getClass().canBeEquipped(*weapon, mActor).first) + if (weapon != end() && weapon->getClass().canBeEquipped(*weapon, actor).first) { // Do not equip ranged weapons, if there is no suitable ammo bool hasAmmo = true; @@ -353,7 +355,7 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) { if (!itemsSlots.second) { - if (weapon->getRefData().getCount() > 1) + if (weapon->getCellRef().getCount() > 1) { unstack(*weapon); } @@ -378,7 +380,8 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) { // Only NPCs can wear armor for now. // For creatures we equip only shields. - if (!mActor.getClass().isNpc()) + const Ptr& actor = getPtr(); + if (!actor.getClass().isNpc()) { autoEquipShield(slots_); return; @@ -389,7 +392,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); - float unarmoredSkill = mActor.getClass().getSkill(mActor, ESM::Skill::Unarmored); + float unarmoredSkill = actor.getClass().getSkill(actor, ESM::Skill::Unarmored); float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); for (ContainerStoreIterator iter(begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end(); @@ -397,7 +400,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) { Ptr test = *iter; - switch (test.getClass().canBeEquipped(test, mActor).first) + switch (test.getClass().canBeEquipped(test, actor).first) { case 0: continue; @@ -406,7 +409,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) } if (iter.getType() == ContainerStore::Type_Armor - && test.getClass().getEffectiveArmorRating(test, mActor) <= std::max(unarmoredRating, 0.f)) + && test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f)) { continue; } @@ -431,8 +434,8 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) if (old.get()->mBase->mData.mType == test.get()->mBase->mData.mType) { - if (old.getClass().getEffectiveArmorRating(old, mActor) - >= test.getClass().getEffectiveArmorRating(test, mActor)) + if (old.getClass().getEffectiveArmorRating(old, actor) + >= test.getClass().getEffectiveArmorRating(test, actor)) // old armor had better armor rating continue; } @@ -475,7 +478,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped { // unstack item pointed to by iterator if required - if (iter->getRefData().getCount() > 1) + if (iter->getCellRef().getCount() > 1) { unstack(*iter); } @@ -494,7 +497,7 @@ void MWWorld::InventoryStore::autoEquipShield(TSlots& slots_) { if (iter->get()->mBase->mData.mType != ESM::Armor::Shield) continue; - if (iter->getClass().canBeEquipped(*iter, mActor).first != 1) + if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1) continue; std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); int slot = shieldSlots.first[0]; @@ -587,7 +590,7 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplac int retCount = ContainerStore::remove(item, count, equipReplacement, resolve); bool wasEquipped = false; - if (!item.getRefData().getCount()) + if (!item.getCellRef().getCount()) { for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { @@ -606,15 +609,16 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplac // If an armor/clothing item is removed, try to find a replacement, // but not for the player nor werewolves, and not if the RemoveItem script command // was used (equipReplacement is false) - if (equipReplacement && wasEquipped && (mActor != MWMechanics::getPlayer()) && mActor.getClass().isNpc() - && !mActor.getClass().getNpcStats(mActor).isWerewolf()) + const Ptr& actor = getPtr(); + if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() + && !actor.getClass().getNpcStats(actor).isWerewolf()) { auto type = item.getType(); if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(); } - if (item.getRefData().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) + if (item.getCellRef().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) { mSelectedEnchantItem = end(); } @@ -639,11 +643,11 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, b // empty this slot mSlots[slot] = end(); - if (it->getRefData().getCount()) + if (it->getCellRef().getCount()) { retval = restack(*it); - if (mActor == MWMechanics::getPlayer()) + if (getPtr() == MWMechanics::getPlayer()) { // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared const ESM::RefId& script = it->getClass().getScript(*it); @@ -686,10 +690,10 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con throw std::runtime_error("attempt to unequip an item that is not currently equipped"); if (count <= 0) throw std::runtime_error("attempt to unequip nothing (count <= 0)"); - if (count > item.getRefData().getCount()) + if (count > item.getCellRef().getCount()) throw std::runtime_error("attempt to unequip more items than equipped"); - if (count == item.getRefData().getCount()) + if (count == item.getCellRef().getCount()) return unequipItem(item); // Move items to an existing stack if possible, otherwise split count items out into a new stack. @@ -698,13 +702,13 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con { if (stacks(*iter, item) && !isEquipped(*iter)) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); - item.getRefData().setCount(subtractItems(item.getRefData().getCount(false), count)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); + item.getCellRef().setCount(subtractItems(item.getCellRef().getCount(false), count)); return iter; } } - return unstack(item, item.getRefData().getCount() - count); + return unstack(item, item.getCellRef().getCount() - count); } MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index c0723d319c..0af6ee2b28 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -81,9 +81,9 @@ namespace MWWorld void fireEquipmentChangedEvent(); void storeEquipmentState( - const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const override; void readEquipmentState( - const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) override; + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) override; ContainerStoreIterator findSlot(int slot) const; @@ -94,9 +94,6 @@ namespace MWWorld InventoryStore& operator=(const InventoryStore& store); - const MWWorld::Ptr& getActor() const { return mActor; } - void setActor(const MWWorld::Ptr& actor) { mActor = actor; } - std::unique_ptr clone() override { auto res = std::make_unique(*this); diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 93470a593a..aaa74f5ef3 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -13,95 +13,124 @@ #include "class.hpp" #include "esmstore.hpp" #include "ptr.hpp" - -MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) - : mClass(&Class::get(type)) - , mRef(cref) - , mData(cref) -{ -} - -MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) - : mClass(&Class::get(type)) - , mRef(cref) - , mData(cref) -{ -} - -MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) - : mClass(&Class::get(type)) - , mRef(cref) - , mData(cref) -{ -} - -void MWWorld::LiveCellRefBase::loadImp(const ESM::ObjectState& state) -{ - mRef = MWWorld::CellRef(state.mRef); - mData = RefData(state, mData.isDeletedByContentFile()); - - Ptr ptr(this); - - if (state.mHasLocals) - { - const ESM::RefId& scriptId = mClass->getScript(ptr); - // Make sure we still have a script. It could have been coming from a content file that is no longer active. - if (!scriptId.empty()) - { - if (const ESM::Script* script - = MWBase::Environment::get().getESMStore()->get().search(scriptId)) - { - try - { - mData.setLocals(*script); - mData.getLocals().read(state.mLocals, scriptId); - } - catch (const std::exception& exception) - { - Log(Debug::Error) << "Error: failed to load state for local script " << scriptId - << " because an exception has been thrown: " << exception.what(); - } - } - } - } - - mClass->readAdditionalState(ptr, state); - - if (!mRef.getSoul().empty() - && !MWBase::Environment::get().getESMStore()->get().search(mRef.getSoul())) - { - Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; - mRef.setSoul(ESM::RefId()); - } - - MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); -} - -void MWWorld::LiveCellRefBase::saveImp(ESM::ObjectState& state) const -{ - mRef.writeState(state); - - ConstPtr ptr(this); - - mData.write(state, mClass->getScript(ptr)); - MWBase::Environment::get().getLuaManager()->saveLocalScripts( - Ptr(const_cast(this)), state.mLuaScripts); - - mClass->writeAdditionalState(ptr, state); -} - -bool MWWorld::LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) -{ - return true; -} - -unsigned int MWWorld::LiveCellRefBase::getType() const -{ - return mClass->getType(); -} +#include "worldmodel.hpp" namespace MWWorld { + LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) + { + } + + LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) + { + } + + LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) + { + } + + LiveCellRefBase::LiveCellRefBase(LiveCellRefBase&& other) noexcept + : mClass(other.mClass) + , mRef(std::move(other.mRef)) + , mData(std::move(other.mData)) + , mWorldModel(std::exchange(other.mWorldModel, nullptr)) + { + } + + LiveCellRefBase::~LiveCellRefBase() + { + if (mWorldModel != nullptr) + mWorldModel->deregisterLiveCellRef(*this); + } + + LiveCellRefBase& LiveCellRefBase::operator=(LiveCellRefBase&& other) noexcept + { + mClass = other.mClass; + mRef = std::move(other.mRef); + mData = std::move(other.mData); + mWorldModel = std::exchange(other.mWorldModel, nullptr); + return *this; + } + + void LiveCellRefBase::loadImp(const ESM::ObjectState& state) + { + mRef = CellRef(state.mRef); + mData = RefData(state, mData.isDeletedByContentFile()); + + Ptr ptr(this); + + if (state.mHasLocals) + { + const ESM::RefId& scriptId = mClass->getScript(ptr); + // Make sure we still have a script. It could have been coming from a content file that is no longer active. + if (!scriptId.empty()) + { + if (const ESM::Script* script + = MWBase::Environment::get().getESMStore()->get().search(scriptId)) + { + try + { + mData.setLocals(*script); + mData.getLocals().read(state.mLocals, scriptId); + } + catch (const std::exception& exception) + { + Log(Debug::Error) << "Error: failed to load state for local script " << scriptId + << " because an exception has been thrown: " << exception.what(); + } + } + } + } + + mClass->readAdditionalState(ptr, state); + + if (!mRef.getSoul().empty() + && !MWBase::Environment::get().getESMStore()->get().search(mRef.getSoul())) + { + Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; + mRef.setSoul(ESM::RefId()); + } + + MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); + } + + void LiveCellRefBase::saveImp(ESM::ObjectState& state) const + { + mRef.writeState(state); + + ConstPtr ptr(this); + + mData.write(state, mClass->getScript(ptr)); + MWBase::Environment::get().getLuaManager()->saveLocalScripts( + Ptr(const_cast(this)), state.mLuaScripts); + + mClass->writeAdditionalState(ptr, state); + } + + bool LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) + { + return true; + } + + unsigned int LiveCellRefBase::getType() const + { + return mClass->getType(); + } + + bool LiveCellRefBase::isDeleted() const + { + return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; + } + std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType) { std::stringstream message; diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index f80690fbe8..026d3edefd 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -17,6 +17,7 @@ namespace MWWorld class Ptr; class ESMStore; class Class; + class WorldModel; template struct LiveCellRef; @@ -29,16 +30,27 @@ namespace MWWorld /** Information about this instance, such as 3D location and rotation * and individual type-dependent data. */ - MWWorld::CellRef mRef; + CellRef mRef; /** runtime-data */ RefData mData; - LiveCellRefBase(unsigned int type, const ESM::CellRef& cref = ESM::CellRef()); + WorldModel* mWorldModel = nullptr; + + LiveCellRefBase(unsigned int type, const ESM::CellRef& cref); LiveCellRefBase(unsigned int type, const ESM4::Reference& cref); LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref); + + LiveCellRefBase(const LiveCellRefBase& other) = default; + + LiveCellRefBase(LiveCellRefBase&& other) noexcept; + /* Need this for the class to be recognized as polymorphic */ - virtual ~LiveCellRefBase() {} + virtual ~LiveCellRefBase(); + + LiveCellRefBase& operator=(const LiveCellRefBase& other) = default; + + LiveCellRefBase& operator=(LiveCellRefBase&& other) noexcept; virtual void load(const ESM::ObjectState& state) = 0; ///< Load state into a LiveCellRef, that has already been initialised with base and class. @@ -59,6 +71,9 @@ namespace MWWorld template static LiveCellRef* dynamicCast(LiveCellRefBase* value); + /// Returns true if the object was either deleted by the content file or by gameplay. + bool isDeleted() const; + protected: void loadImp(const ESM::ObjectState& state); ///< Load state into a LiveCellRef, that has already been initialised with base and @@ -129,12 +144,6 @@ namespace MWWorld { } - LiveCellRef(const X* b = nullptr) - : LiveCellRefBase(X::sRecordId) - , mBase(b) - { - } - // The object that this instance is based on. const X* mBase; diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 8d9a282791..8f5dc63dfb 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -24,7 +24,7 @@ namespace bool operator()(const MWWorld::Ptr& ptr) { - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) return true; const ESM::RefId& script = ptr.getClass().getScript(ptr); @@ -152,10 +152,10 @@ void MWWorld::LocalScripts::clearCell(CellStore* cell) } } -void MWWorld::LocalScripts::remove(RefData* ref) +void MWWorld::LocalScripts::remove(const MWWorld::CellRef* ref) { for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) - if (&(iter->second.getRefData()) == ref) + if (&(iter->second.getCellRef()) == ref) { if (iter == mIter) ++mIter; @@ -177,3 +177,8 @@ void MWWorld::LocalScripts::remove(const Ptr& ptr) break; } } + +bool MWWorld::LocalScripts::isRunning(const ESM::RefId& scriptName, const Ptr& ptr) const +{ + return std::ranges::find(mScripts, std::pair(scriptName, ptr)) != mScripts.end(); +} diff --git a/apps/openmw/mwworld/localscripts.hpp b/apps/openmw/mwworld/localscripts.hpp index 4bd2abeb84..fd7ae23edf 100644 --- a/apps/openmw/mwworld/localscripts.hpp +++ b/apps/openmw/mwworld/localscripts.hpp @@ -41,10 +41,13 @@ namespace MWWorld void clearCell(CellStore* cell); ///< Remove all scripts belonging to \a cell. - void remove(RefData* ref); + void remove(const MWWorld::CellRef* ref); void remove(const Ptr& ptr); ///< Remove script for given reference (ignored if reference does not have a script listed). + + bool isRunning(const ESM::RefId&, const Ptr&) const; + ///< Is the local script running?. }; } diff --git a/apps/openmw/mwworld/magiceffects.cpp b/apps/openmw/mwworld/magiceffects.cpp index 8b7ad79db2..38f17677ef 100644 --- a/apps/openmw/mwworld/magiceffects.cpp +++ b/apps/openmw/mwworld/magiceffects.cpp @@ -11,6 +11,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwmechanics/magiceffects.hpp" @@ -46,8 +47,8 @@ namespace MWWorld for (auto& effect : spell->mEffects.mList) { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; + if (effect.mData.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mData.mAttribute] = oldStats.mWorsenings; } creatureStats.mCorprusSpells[id] = stats; } @@ -58,30 +59,30 @@ namespace MWWorld if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power) continue; ESM::ActiveSpells::ActiveSpellParams params; - params.mId = id; + params.mSourceSpellId = id; params.mDisplayName = spell->mName; params.mCasterActorId = creatureStats.mActorId; if (spell->mData.mType == ESM::Spell::ST_Ability) - params.mType = ESM::ActiveSpells::Type_Ability; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Ability_Flags; else - params.mType = ESM::ActiveSpells::Type_Permanent; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Permanent_Flags; params.mWorsenings = -1; params.mNextWorsening = ESM::TimeStamp(); - int effectIndex = 0; for (const auto& enam : spell->mEffects.mList) { - if (oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end()) + if (oldParams.mPurgedEffects.find(enam.mIndex) == oldParams.mPurgedEffects.end()) { ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mDuration = -1; effect.mTimeLeft = -1; - effect.mEffectIndex = effectIndex; - auto rand = oldParams.mEffectRands.find(effectIndex); + effect.mEffectIndex = enam.mIndex; + auto rand = oldParams.mEffectRands.find(enam.mIndex); if (rand != oldParams.mEffectRands.end()) { - float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; + float magnitude + = (enam.mData.mMagnMax - enam.mData.mMagnMin) * rand->second + enam.mData.mMagnMin; effect.mMagnitude = magnitude; effect.mMinMagnitude = magnitude; effect.mMaxMagnitude = magnitude; @@ -92,23 +93,25 @@ namespace MWWorld else { effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; effect.mFlags = ESM::ActiveEffect::Flag_None; } params.mEffects.emplace_back(effect); } - effectIndex++; } creatureStats.mActiveSpells.mSpells.emplace_back(params); } - std::multimap equippedItems; + std::multimap equippedItems; for (std::size_t i = 0; i < inventory.mItems.size(); ++i) { - const ESM::ObjectState& item = inventory.mItems[i]; + ESM::ObjectState& item = inventory.mItems[i]; auto slot = inventory.mEquipmentSlots.find(i); if (slot != inventory.mEquipmentSlots.end()) - equippedItems.emplace(item.mRef.mRefID, slot->second); + { + MWBase::Environment::get().getWorldModel()->assignSaveFileRefNum(item.mRef); + equippedItems.emplace(item.mRef.mRefID, item.mRef.mRefNum); + } } for (const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) { @@ -132,30 +135,28 @@ namespace MWWorld if (!enchantment) continue; ESM::ActiveSpells::ActiveSpellParams params; - params.mId = id; - params.mDisplayName = name; + params.mSourceSpellId = id; + params.mDisplayName = std::move(name); params.mCasterActorId = creatureStats.mActorId; - params.mType = ESM::ActiveSpells::Type_Enchantment; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Enchantment_Flags; params.mWorsenings = -1; params.mNextWorsening = ESM::TimeStamp(); - for (std::size_t effectIndex = 0; - effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) + for (const auto& enam : enchantment->mEffects.mList) { - const auto& enam = enchantment->mEffects.mList[effectIndex]; - auto [random, multiplier] = oldMagnitudes[effectIndex]; - float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; + auto [random, multiplier] = oldMagnitudes[enam.mIndex]; + float magnitude = (enam.mData.mMagnMax - enam.mData.mMagnMin) * random + enam.mData.mMagnMin; magnitude *= multiplier; if (magnitude <= 0) continue; ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; + effect.mEffectId = enam.mData.mEffectID; effect.mMagnitude = magnitude; effect.mMinMagnitude = magnitude; effect.mMaxMagnitude = magnitude; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mDuration = -1; effect.mTimeLeft = -1; - effect.mEffectIndex = static_cast(effectIndex); + effect.mEffectIndex = enam.mIndex; // Prevent recalculation of resistances and don't reflect or absorb the effect effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; @@ -164,7 +165,7 @@ namespace MWWorld auto [begin, end] = equippedItems.equal_range(id); for (auto it = begin; it != end; ++it) { - params.mItem = { static_cast(it->second), 0 }; + params.mItem = it->second; creatureStats.mActiveSpells.mSpells.emplace_back(params); } } @@ -172,7 +173,7 @@ namespace MWWorld { auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), - [&](const auto& params) { return params.mId == spell.first; }); + [&](const auto& params) { return params.mSourceSpellId == spell.first; }); if (it != creatureStats.mActiveSpells.mSpells.end()) { it->mNextWorsening = spell.second.mNextWorsening; @@ -188,7 +189,7 @@ namespace MWWorld continue; for (auto& params : creatureStats.mActiveSpells.mSpells) { - if (params.mId == key.mSourceId) + if (params.mSourceSpellId == key.mSourceId) { bool found = false; for (auto& effect : params.mEffects) @@ -232,4 +233,28 @@ namespace MWWorld for (auto& setting : creatureStats.mAiSettings) setting.mMod = 0.f; } + + // Versions 17-27 wrote an equipment slot index to mItem + void convertEnchantmentSlots(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory) + { + for (auto& activeSpell : creatureStats.mActiveSpells.mSpells) + { + if (!activeSpell.mItem.isSet()) + continue; + if (activeSpell.mFlags & ESM::ActiveSpells::Flag_Equipment) + { + std::int64_t slotIndex = activeSpell.mItem.mIndex; + auto slot = std::find_if(inventory.mEquipmentSlots.begin(), inventory.mEquipmentSlots.end(), + [=](const auto& entry) { return entry.second == slotIndex; }); + if (slot != inventory.mEquipmentSlots.end() && slot->first < inventory.mItems.size()) + { + ESM::CellRef& ref = inventory.mItems[slot->first].mRef; + MWBase::Environment::get().getWorldModel()->assignSaveFileRefNum(ref); + activeSpell.mItem = ref.mRefNum; + continue; + } + } + activeSpell.mItem = {}; + } + } } diff --git a/apps/openmw/mwworld/magiceffects.hpp b/apps/openmw/mwworld/magiceffects.hpp index aefcd056e0..3acb14fff2 100644 --- a/apps/openmw/mwworld/magiceffects.hpp +++ b/apps/openmw/mwworld/magiceffects.hpp @@ -14,6 +14,8 @@ namespace MWWorld ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr); void convertStats(ESM::CreatureStats& creatureStats); + + void convertEnchantmentSlots(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory); } #endif diff --git a/apps/openmw/mwworld/manualref.cpp b/apps/openmw/mwworld/manualref.cpp index e9fd02e6f5..81ab4b5419 100644 --- a/apps/openmw/mwworld/manualref.cpp +++ b/apps/openmw/mwworld/manualref.cpp @@ -19,87 +19,91 @@ namespace refValue = MWWorld::LiveCellRef(cellRef, base); ptrValue = MWWorld::Ptr(&std::any_cast&>(refValue), nullptr); } + + template + void create( + const MWWorld::Store& list, const MWWorld::Ptr& templatePtr, std::any& refValue, MWWorld::Ptr& ptrValue) + { + refValue = *static_cast*>(templatePtr.getBase()); + ptrValue = MWWorld::Ptr(&std::any_cast&>(refValue), nullptr); + } + + template + void visitRefStore(const MWWorld::ESMStore& store, ESM::RefId name, F func) + { + switch (store.find(name)) + { + case ESM::REC_ACTI: + return func(store.get()); + case ESM::REC_ALCH: + return func(store.get()); + case ESM::REC_APPA: + return func(store.get()); + case ESM::REC_ARMO: + return func(store.get()); + case ESM::REC_BOOK: + return func(store.get()); + case ESM::REC_CLOT: + return func(store.get()); + case ESM::REC_CONT: + return func(store.get()); + case ESM::REC_CREA: + return func(store.get()); + case ESM::REC_DOOR: + return func(store.get()); + case ESM::REC_INGR: + return func(store.get()); + case ESM::REC_LEVC: + return func(store.get()); + case ESM::REC_LEVI: + return func(store.get()); + case ESM::REC_LIGH: + return func(store.get()); + case ESM::REC_LOCK: + return func(store.get()); + case ESM::REC_MISC: + return func(store.get()); + case ESM::REC_NPC_: + return func(store.get()); + case ESM::REC_PROB: + return func(store.get()); + case ESM::REC_REPA: + return func(store.get()); + case ESM::REC_STAT: + return func(store.get()); + case ESM::REC_WEAP: + return func(store.get()); + case ESM::REC_BODY: + return func(store.get()); + case ESM::REC_STAT4: + return func(store.get()); + case ESM::REC_TERM4: + return func(store.get()); + case 0: + throw std::logic_error( + "failed to create manual cell ref for " + name.toDebugString() + " (unknown ID)"); + + default: + throw std::logic_error( + "failed to create manual cell ref for " + name.toDebugString() + " (unknown type)"); + } + } } MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const ESM::RefId& name, const int count) { - switch (store.find(name)) - { - case ESM::REC_ACTI: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_ALCH: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_APPA: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_ARMO: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_BOOK: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_CLOT: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_CONT: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_CREA: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_DOOR: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_INGR: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_LEVC: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_LEVI: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_LIGH: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_LOCK: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_MISC: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_NPC_: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_PROB: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_REPA: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_STAT: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_WEAP: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_BODY: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_STAT4: - create(store.get(), name, mRef, mPtr); - break; - case ESM::REC_TERM4: - create(store.get(), name, mRef, mPtr); - break; - case 0: - throw std::logic_error("failed to create manual cell ref for " + name.toDebugString() + " (unknown ID)"); + auto cb = [&](const auto& store) { create(store, name, mRef, mPtr); }; + visitRefStore(store, name, cb); - default: - throw std::logic_error("failed to create manual cell ref for " + name.toDebugString() + " (unknown type)"); - } - - mPtr.getRefData().setCount(count); + mPtr.getCellRef().setCount(count); +} + +MWWorld::ManualRef::ManualRef(const ESMStore& store, const Ptr& template_, const int count) +{ + auto cb = [&](const auto& store) { create(store, template_, mRef, mPtr); }; + visitRefStore(store, template_.getCellRef().getRefId(), cb); + + mPtr.getCellRef().setCount(count); + mPtr.getCellRef().unsetRefNum(); + mPtr.getRefData().setLuaScripts(nullptr); } diff --git a/apps/openmw/mwworld/manualref.hpp b/apps/openmw/mwworld/manualref.hpp index 8356fd0a03..4611ed95bb 100644 --- a/apps/openmw/mwworld/manualref.hpp +++ b/apps/openmw/mwworld/manualref.hpp @@ -7,9 +7,11 @@ namespace MWWorld { - /// \brief Manually constructed live cell ref + /// \brief Manually constructed live cell ref. The resulting Ptr shares its lifetime with this ManualRef and must + /// not be used past its end. class ManualRef { + // Stores the ref (LiveCellRef) by value. std::any mRef; Ptr mPtr; @@ -18,6 +20,7 @@ namespace MWWorld public: ManualRef(const MWWorld::ESMStore& store, const ESM::RefId& name, const int count = 1); + ManualRef(const MWWorld::ESMStore& store, const MWWorld::Ptr& template_, const int count = 1); const Ptr& getPtr() const { return mPtr; } }; diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 0d7afb559f..b776f27f06 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -36,8 +36,20 @@ namespace MWWorld { + namespace + { + ESM::CellRef makePlayerCellRef() + { + ESM::CellRef result; + result.blank(); + result.mRefID = ESM::RefId::stringRefId("Player"); + return result; + } + } + Player::Player(const ESM::NPC* player) - : mCellStore(nullptr) + : mPlayer(makePlayerCellRef(), player) + , mCellStore(nullptr) , mLastKnownExteriorPosition(0, 0, 0) , mMarkedPosition(ESM::Position()) , mMarkedCell(nullptr) @@ -46,11 +58,6 @@ namespace MWWorld , mPaidCrimeId(-1) , mJumping(false) { - ESM::CellRef cellRef; - cellRef.blank(); - cellRef.mRefID = ESM::RefId::stringRefId("Player"); - mPlayer = LiveCellRef(cellRef, player); - ESM::Position playerPos = mPlayer.mData.getPosition(); playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0; mPlayer.mData.setPosition(playerPos); @@ -183,8 +190,7 @@ namespace MWWorld MWWorld::Ptr player = getPlayer(); const MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && playerStats.isParalyzed()) || playerStats.getKnockedDown() || playerStats.isDead()) + if (playerStats.isParalyzed() || playerStats.getKnockedDown() || playerStats.isDead()) return; MWWorld::Ptr toActivate = MWBase::Environment::get().getWorld()->getFacedObject(); @@ -243,6 +249,11 @@ namespace MWWorld void Player::clear() { + ESM::CellRef cellRef; + cellRef.blank(); + cellRef.mRefID = ESM::RefId::stringRefId("Player"); + cellRef.mRefNum = mPlayer.mRef.getRefNum(); + mPlayer = LiveCellRef(cellRef, mPlayer.mBase); mCellStore = nullptr; mSign = ESM::RefId(); mMarkedCell = nullptr; @@ -317,7 +328,12 @@ namespace MWWorld convertMagicEffects( player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats); else if (reader.getFormatVersion() <= ESM::MaxOldCreatureStatsFormatVersion) + { convertStats(player.mObject.mCreatureStats); + convertEnchantmentSlots(player.mObject.mCreatureStats, player.mObject.mInventory); + } + else if (reader.getFormatVersion() <= ESM::MaxActiveSpellSlotIndexFormatVersion) + convertEnchantmentSlots(player.mObject.mCreatureStats, player.mObject.mInventory); if (!player.mObject.mEnabled) { @@ -325,6 +341,7 @@ namespace MWWorld player.mObject.mEnabled = true; } + MWBase::Environment::get().getWorldModel()->deregisterLiveCellRef(mPlayer); mPlayer.load(player.mObject); for (size_t i = 0; i < mSaveAttributes.size(); ++i) @@ -334,12 +351,7 @@ namespace MWWorld if (player.mObject.mNpcStats.mIsWerewolf) { - if (player.mObject.mNpcStats.mWerewolfDeprecatedData) - { - saveStats(); - setWerewolfStats(); - } - else if (reader.getFormatVersion() <= ESM::MaxOldSkillsAndAttributesFormatVersion) + if (reader.getFormatVersion() <= ESM::MaxOldSkillsAndAttributesFormatVersion) { setWerewolfStats(); if (player.mSetWerewolfAcrobatics) diff --git a/apps/openmw/mwworld/positioncellgrid.hpp b/apps/openmw/mwworld/positioncellgrid.hpp new file mode 100644 index 0000000000..cbb8e3300a --- /dev/null +++ b/apps/openmw/mwworld/positioncellgrid.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_APPS_OPENMW_MWWORLD_POSITIONCELLGRID_H +#define OPENMW_APPS_OPENMW_MWWORLD_POSITIONCELLGRID_H + +#include +#include + +namespace MWWorld +{ + struct PositionCellGrid + { + osg::Vec3f mPosition; + osg::Vec4i mCellBounds; + }; +} + +#endif diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index d873f16a59..b019dabdfe 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -15,6 +15,9 @@ #include #include +#include +#include + #include #include #include @@ -79,18 +82,17 @@ namespace int count = 0; speed = 0.0f; ESM::EffectList projectileEffects; - for (std::vector::const_iterator iter(effects->mList.begin()); iter != effects->mList.end(); - ++iter) + for (const ESM::IndexedENAMstruct& effect : effects->mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(iter->mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); // Speed of multi-effect projectiles should be the average of the constituent effects, // based on observation of the original engine. speed += magicEffect->mData.mSpeed; count++; - if (iter->mRange != ESM::RT_Target) + if (effect.mData.mRange != ESM::RT_Target) continue; if (magicEffect->mBolt.empty()) @@ -106,7 +108,7 @@ namespace ->get() .find(magicEffect->mData.mSchool) ->mSchool->mBoltSound); - projectileEffects.mList.push_back(*iter); + projectileEffects.mList.push_back(effect); } if (count != 0) @@ -117,7 +119,7 @@ namespace { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find( - effects->mList.begin()->mEffectID); + effects->mList.begin()->mData.mEffectID); texture = magicEffect->mParticle; } @@ -136,10 +138,10 @@ namespace { // Calculate combined light diffuse color from magical effects osg::Vec4 lightDiffuseColor; - for (const ESM::ENAMstruct& enam : effects.mList) + for (const ESM::IndexedENAMstruct& enam : effects.mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(enam.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(enam.mData.mEffectID); lightDiffuseColor += magicEffect->getColor(); } int numberOfEffects = effects.mList.size(); @@ -187,8 +189,8 @@ namespace MWWorld float mRotateSpeed; }; - void ProjectileManager::createModel(State& state, const std::string& model, const osg::Vec3f& pos, - const osg::Quat& orient, bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture) + void ProjectileManager::createModel(State& state, VFS::Path::NormalizedView model, const osg::Vec3f& pos, + const osg::Quat& orient, bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, const std::string& texture) { state.mNode = new osg::PositionAttitudeTransform; state.mNode->setNodeMask(MWRender::Mask_Effect); @@ -209,8 +211,6 @@ namespace MWWorld if (state.mIdMagic.size() > 1) { - const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter) { std::ostringstream nodeName; @@ -222,7 +222,8 @@ namespace MWWorld attachTo->accept(findVisitor); if (findVisitor.mFoundNode) mResourceSystem->getSceneManager()->getInstance( - Misc::ResourceHelpers::correctMeshPath(weapon->mModel, vfs), findVisitor.mFoundNode); + Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(weapon->mModel)), + findVisitor.mFoundNode); } } @@ -254,7 +255,7 @@ namespace MWWorld SceneUtil::AssignControllerSourcesVisitor assignVisitor(state.mEffectAnimationTime); state.mNode->accept(assignVisitor); - MWRender::overrideFirstRootTexture(texture, mResourceSystem, std::move(projectile)); + MWRender::overrideFirstRootTexture(texture, mResourceSystem, *projectile); } void ProjectileManager::update(State& state, float duration) @@ -313,7 +314,7 @@ namespace MWWorld osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); - auto model = ptr.getClass().getModel(ptr); + VFS::Path::Normalized model = ptr.getClass().getCorrectedModel(ptr); createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); @@ -329,9 +330,8 @@ namespace MWWorld // shape if (state.mIdMagic.size() > 1) { - model = Misc::ResourceHelpers::correctMeshPath( - MWBase::Environment::get().getESMStore()->get().find(state.mIdMagic[1])->mModel, - MWBase::Environment::get().getResourceSystem()->getVFS()); + model = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized( + MWBase::Environment::get().getESMStore()->get().find(state.mIdMagic[1])->mModel)); } state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); state.mToDelete = false; @@ -354,7 +354,7 @@ namespace MWWorld MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), projectile.getCellRef().getRefId()); MWWorld::Ptr ptr = ref.getPtr(); - const auto model = ptr.getClass().getModel(ptr); + const VFS::Path::Normalized model = ptr.getClass().getCorrectedModel(ptr); createModel(state, model, pos, orient, false, false, osg::Vec4(0, 0, 0, 0)); if (!ptr.getClass().getEnchantment(ptr).empty()) SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); @@ -437,7 +437,7 @@ namespace MWWorld MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { - if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) + if (caster.getCellRef().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) { cleanupMagicBolt(magicBoltState); continue; @@ -453,7 +453,7 @@ namespace MWWorld { const auto npc = caster.get()->mBase; const auto race = store.get().find(npc->mRace); - speed *= npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; + speed *= npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; } osg::Vec3f direction = orient * osg::Vec3f(0, 1, 0); direction.normalize(); @@ -694,20 +694,22 @@ namespace MWWorld state.mAttackStrength = esm.mAttackStrength; state.mToDelete = false; - std::string model; + VFS::Path::Normalized model; try { MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), esm.mId); MWWorld::Ptr ptr = ref.getPtr(); - model = ptr.getClass().getModel(ptr); + model = ptr.getClass().getCorrectedModel(ptr); int weaponType = ptr.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown; state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false); } - catch (...) + catch (const std::exception& e) { + Log(Debug::Warning) << "Failed to add projectile for " << esm.mId + << " while reading projectile record: " << e.what(); return true; } @@ -735,10 +737,10 @@ namespace MWWorld state.mEffects = getMagicBoltData( state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); } - catch (...) + catch (const std::exception& e) { - Log(Debug::Warning) << "Warning: Failed to recreate magic projectile from saved data (id \"" - << state.mSpellId << "\" no longer exists?)"; + Log(Debug::Warning) << "Failed to recreate magic projectile for " << esm.mId << " and spell " + << state.mSpellId << " while reading projectile record: " << e.what(); return true; } @@ -747,15 +749,17 @@ namespace MWWorld // file's effect list, which is already trimmed of non-projectile // effects. We need to use the stored value. - std::string model; + VFS::Path::Normalized model; try { MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); - model = ptr.getClass().getModel(ptr); + model = ptr.getClass().getCorrectedModel(ptr); } - catch (...) + catch (const std::exception& e) { + Log(Debug::Warning) << "Failed to get model for " << state.mIdMagic.at(0) + << " while reading projectile record: " << e.what(); return true; } diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index 65254a9110..3003136ffc 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -7,6 +7,7 @@ #include #include +#include #include "../mwbase/soundmanager.hpp" @@ -135,8 +136,8 @@ namespace MWWorld void moveProjectiles(float dt); void moveMagicBolts(float dt); - void createModel(State& state, const std::string& model, const osg::Vec3f& pos, const osg::Quat& orient, - bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture = ""); + void createModel(State& state, VFS::Path::NormalizedView model, const osg::Vec3f& pos, const osg::Quat& orient, + bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, const std::string& texture = ""); void update(State& state, float duration); void operator=(const ProjectileManager&); diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp index 1421b51a2a..8b3f77e805 100644 --- a/apps/openmw/mwworld/ptr.cpp +++ b/apps/openmw/mwworld/ptr.cpp @@ -6,21 +6,6 @@ namespace MWWorld { - - std::string Ptr::toString() const - { - std::string res = "object"; - if (getRefData().isDeleted()) - res = "deleted object"; - res.append(getCellRef().getRefNum().toString()); - res.append(" ("); - res.append(getTypeDescription()); - res.append(", "); - res.append(getCellRef().getRefId().toDebugString()); - res.append(")"); - return res; - } - SafePtr::SafePtr(const Ptr& ptr) : mId(ptr.getCellRef().getRefNum()) , mPtr(ptr) diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index 1adaa3cbf9..f434d8b0dd 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -98,6 +98,20 @@ namespace MWWorld return mContainerStore; } + std::string toString() const + { + if (mRef == nullptr) + return "null object"; + std::string result = mRef->isDeleted() ? "deleted object" : "object"; + result += mRef->mRef.getRefNum().toString(); + result += " ("; + result += mRef->getTypeDescription(); + result += ", "; + result += mRef->mRef.getRefId().toDebugString(); + result += ")"; + return result; + } + template